diff --git a/.env.example b/.env.example index dddbb4be4c260..1ca8f9658dbef 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,11 @@ LOCAL_DIR=src # Valid options are 'on' and 'off'. LOCAL_NGINX_COMPRESSION=off +# How long browsers may cache static assets served by the web server, as an nginx `expires` value. +# Valid options are 'off' and a duration such as '1y'. A far-future duration matches typical +# production behavior, but edited CSS and JavaScript is then served stale until a hard reload. +LOCAL_NGINX_STATIC_EXPIRES=off + # The PHP version to use. Valid options are 'latest', and '{version}-fpm'. LOCAL_PHP=latest diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 9b3144df5c64d..0000000000000 --- a/.eslintignore +++ /dev/null @@ -1,26 +0,0 @@ -# Files and folders related to build/test tools including generated files -/build -/node_modules -/tests -/vendor -/tools -/jsdoc -/artifacts -/coverage -.cache/* -/src/wp-includes/blocks/**/*.js -/src/wp-includes/blocks/**/*.js.map -/src/wp-admin/js -/src/wp-includes/js - -# Excluded files and folders based on `jsdoc.conf.json` exclusions -/src/js/_enqueues/vendor - -# Themes -src/wp-content/themes - -# Files and folders that get created in wp-content -/src/wp-content/plugins -/src/wp-content/mu-plugins -/src/wp-content/upgrade -/src/wp-content/uploads diff --git a/.eslintrc-jsdoc.js b/.eslintrc-jsdoc.js index 6f7351ec08945..7815cb1395e80 100644 --- a/.eslintrc-jsdoc.js +++ b/.eslintrc-jsdoc.js @@ -1,29 +1,88 @@ -module.exports = { - rules: { - 'require-jsdoc': 'off', - 'valid-jsdoc': [ 'error', { - prefer: { - arg: 'param', - argument: 'param', - extends: 'augments', - returns: 'return', +/** + * ESLint v10 flat config file for JSDoc linting. + * This config is based on the original valid-jsdoc rules. + */ + +const jsdocPlugin = require( 'eslint-plugin-jsdoc' ); + +module.exports = [ + { + plugins: { + jsdoc: jsdocPlugin, + }, + settings: { + jsdoc: { + tagNamePreference: { + 'arg': 'param', + 'argument': 'param', + 'extends': 'augments', + 'returns': 'return', + }, + preferredTypes: { + 'array': 'Array', + 'bool': 'boolean', + 'Boolean': 'boolean', + 'float': 'number', + 'Float': 'number', + 'function': 'Function', + 'int': 'number', + 'integer': 'number', + 'Integer': 'number', + 'Number': 'number', + 'object': 'Object', + 'String': 'string', + 'Void': 'void', + }, }, - preferType: { - array: 'Array', - bool: 'boolean', - Boolean: 'boolean', - float: 'number', - Float: 'number', - int: 'number', - integer: 'number', - Integer: 'number', - Number: 'number', - object: 'Object', - String: 'string', - Void: 'void', - }, - requireParamDescription: false, - requireReturn: false, - } ], + }, + rules: { + 'jsdoc/check-alignment': 'error', + 'jsdoc/check-indentation': [ 'error', { + excludeTags: [ + 'deprecated', + 'example', + 'param', + 'property', + 'return', + 'since', + 'todo', + ], + } ], + 'jsdoc/check-line-alignment': [ 'error', 'always', { + tags: [ 'param', 'property' ], + } ], + 'jsdoc/check-param-names': 'error', + 'jsdoc/check-property-names': 'error', + 'jsdoc/check-syntax': 'error', + 'jsdoc/check-tag-names': [ 'error', { + definedTags: [ + 'memberOf', + 'output', + 'ticket', + 'link', + ], + } ], + 'jsdoc/check-types': 'error', + 'jsdoc/no-blank-blocks': 'error', + 'jsdoc/no-blank-block-descriptions': 'error', + 'jsdoc/require-description': 'error', + 'jsdoc/require-hyphen-before-param-description': [ 'error', 'never' ], + 'jsdoc/require-jsdoc': 'error', + 'jsdoc/require-param': [ 'error', { + enableFixer: false, + } ], + 'jsdoc/require-param-description': 'error', + 'jsdoc/require-param-name': 'error', + 'jsdoc/require-param-type': 'error', + 'jsdoc/require-property': 'error', + 'jsdoc/require-property-name': 'error', + 'jsdoc/require-property-description': 'error', + 'jsdoc/require-property-type': 'error', + 'jsdoc/require-returns': 'error', + 'jsdoc/require-returns-check': 'error', + 'jsdoc/require-returns-description': 'error', + 'jsdoc/require-returns-type': 'error', + 'jsdoc/valid-types': 'error', + }, }, -}; +]; diff --git a/.github/actions/setup-npm/action.yml b/.github/actions/setup-npm/action.yml new file mode 100644 index 0000000000000..fba5bd0257bcc --- /dev/null +++ b/.github/actions/setup-npm/action.yml @@ -0,0 +1,55 @@ +name: 'Set up npm' +description: 'Installs the npm version required by the devEngines field of package.json, so every job resolves dependencies identically. Keeps the bundled npm when the field is absent.' + +inputs: + working-directory: + description: 'Optional. The directory holding the package.json to read the required npm version from. Defaults to the workspace root.' + required: false + default: '.' + cache: + description: 'Optional. Whether to restore and save the npm cache. Defaults to true.' + required: false + default: 'true' + +runs: + using: 'composite' + steps: + - name: Install npm + id: npm + env: + WORKING_DIRECTORY: ${{ inputs.working-directory }} + run: | + NPM_VERSION="$(jq -r '.devEngines.packageManager.version // empty' package.json)" + if [ -n "$NPM_VERSION" ]; then + # Contributors need the floor; CI stays on that major's newest release. + npm install --global "npm@${NPM_VERSION/>=/^}" + echo "Required npm version: $NPM_VERSION" + else + echo "No devEngines.packageManager.version, keeping the bundled npm." + fi + echo "Installed npm version: $(npm --version)" + + # actions/setup-node cannot restore the npm cache: it locates the + # directory with `npm config get cache`, which runs before this step + # and fails the devEngines check under the bundled npm. + echo "cache-dir=$(npm config get cache | tr -d '\r')" >> "$GITHUB_OUTPUT" + + # Scopes the cache key to this package and keeps the hashFiles patterns + # workspace-relative and free of a "./" prefix, so they match the same + # files as every other cache key. + PATH_PREFIX="" + if [ "$WORKING_DIRECTORY" != '.' ] && [ -n "$WORKING_DIRECTORY" ]; then + PATH_PREFIX="${WORKING_DIRECTORY%/}/" + fi + echo "path-prefix=$PATH_PREFIX" >> "$GITHUB_OUTPUT" + working-directory: ${{ inputs.working-directory }} + shell: bash + + - name: Cache npm downloads + if: ${{ inputs.cache == 'true' }} + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ${{ steps.npm.outputs.cache-dir }} + key: npm-cache-${{ runner.os }}-${{ runner.arch }}-${{ steps.npm.outputs.path-prefix }}lock-${{ hashFiles(format('{0}package-lock.json', steps.npm.outputs.path-prefix), format('{0}patches/**', steps.npm.outputs.path-prefix)) }} + restore-keys: | + npm-cache-${{ runner.os }}-${{ runner.arch }}-${{ steps.npm.outputs.path-prefix }}lock- diff --git a/.github/actions/smoke-check/action.yml b/.github/actions/smoke-check/action.yml new file mode 100644 index 0000000000000..4a115e0f6d754 --- /dev/null +++ b/.github/actions/smoke-check/action.yml @@ -0,0 +1,20 @@ +name: 'Smoke check a WordPress installation' +description: 'Serves an installed single site with PHP built-in web server and checks that its front end, REST API, and admin respond.' + +inputs: + admin-user: + description: 'The username of an administrator account on the site.' + required: true + admin-password: + description: 'The password of the administrator account.' + required: true + +runs: + using: 'composite' + steps: + - name: Serve the site and check it + shell: bash + run: bash "${GITHUB_ACTION_PATH}/smoke-check.sh" + env: + WP_ADMIN_USER: ${{ inputs.admin-user }} + WP_ADMIN_PASSWORD: ${{ inputs.admin-password }} diff --git a/.github/actions/smoke-check/smoke-check.sh b/.github/actions/smoke-check/smoke-check.sh new file mode 100755 index 0000000000000..d3ab33e068f9e --- /dev/null +++ b/.github/actions/smoke-check/smoke-check.sh @@ -0,0 +1,117 @@ +#!/bin/bash +# +# Smoke checks an installed WordPress single site over HTTP. +# +# Runs from the root of the installation, with WP-CLI available. Multisite is not supported because +# a network's domain lives in wp-config.php and the database, not just an option. + +set -euo pipefail + +: "${RUNNER_TEMP:?}" "${WP_ADMIN_USER:?}" "${WP_ADMIN_PASSWORD:?}" + +site_port=8889 +site_url="http://127.0.0.1:${site_port}" +server_log="${RUNNER_TEMP}/server.log" +jar="${RUNNER_TEMP}/cookies.txt" +: > "${jar}" +response="${RUNNER_TEMP}/response.txt" +code="${RUNNER_TEMP}/http-code.txt" +http_code='' + +version="$(wp core version)" + +# Nothing undoes these, so the site is only good for this check afterwards. +wp option update home "${site_url}" +wp option update siteurl "${site_url}" +# A fresh install shows posts here already, but the front page check below needs it. +wp option update show_on_front posts +# Both make requests no check asked for, and the update checks call api.wordpress.org. +wp config set DISABLE_WP_CRON true --raw +wp config set WP_HTTP_BLOCK_EXTERNAL true --raw + +# opcache counts php -S as a web SAPI, so the runner's JIT runs here and segfaults. +php -d opcache.jit=disable -S "127.0.0.1:${site_port}" -t . > "${server_log}" 2>&1 & +server_pid=$! + +stop_server() { + local status=$? + kill "${server_pid}" 2> /dev/null || true + wait "${server_pid}" 2> /dev/null || true + [ "${status}" -eq 0 ] || cat "${server_log}" || true + exit "${status}" +} +trap stop_server EXIT + +# No connection prints 000 and exits 7, which set -e would take as fatal. +deadline=$(( SECONDS + 30 )) +while :; do + ready_code="$(curl --silent --max-time 5 --output /dev/null --write-out '%{http_code}' "${site_url}/" || true)" + [ "${ready_code}" = '000' ] || break + [ "${SECONDS}" -lt "${deadline}" ] || { echo '::error::the server never answered'; exit 1; } + sleep 1 +done + +fail() { + echo "::error::${1}" + tail -n 20 "${response}" + exit 1 +} + +# An HTTP error returns to the caller. No reply at all is fatal. +fetch() { + local path="${1}" status=0 + shift + : > "${response}" + curl --silent --show-error --max-time 30 --output "${response}" \ + --write-out '%{http_code}' --cookie "${jar}" --cookie-jar "${jar}" \ + "$@" "${site_url}${path}" > "${code}" || status=$? + http_code="$(cat "${code}")" + [ "${status}" -eq 0 ] || [ "${status}" -eq 22 ] \ + || fail "${path} got no reply and curl exited ${status}" + return "${status}" +} + +check() { + local path="${1}" marker status=0 + shift + fetch "${path}" --fail-with-body || status=$? + [ "${status}" -eq 0 ] || fail "${path} returned HTTP ${http_code}" + for marker in "$@"; do + grep -qF -e "${marker}" "${response}" || fail "${path} did not contain: ${marker}" + done + echo "ok ${path}" +} + +# A fatal mid-page still returns 200, so every check also asks for the closing tag. +check '/' "content=\"WordPress ${version}\"" 'Hello world!' '' + +fetch '/?rest_route=/' --fail-with-body || fail "/?rest_route=/ returned HTTP ${http_code}" +jq -e --arg url "${site_url}" '.url == $url and ( .namespaces | index( "wp/v2" ) )' "${response}" > /dev/null \ + || fail 'the REST index was not the expected JSON' +echo 'ok /?rest_route=/' + +# It prints this for an empty database too, which the checks above rule out. +check '/wp-admin/upgrade.php' 'No Update Required' '' + +fetch '/?p=99999999' || true +[ "${http_code}" = '404' ] || fail "a missing post returned ${http_code}, expected 404" +grep -qF '' "${response}" || fail 'the 404 page was cut short' +echo 'ok 404 handling' + +login_status=0 +fetch '/wp-login.php' --fail-with-body \ + --data-urlencode "log=${WP_ADMIN_USER}" --data-urlencode "pwd=${WP_ADMIN_PASSWORD}" || login_status=$? +[ "${login_status}" -eq 0 ] || fail "logging in returned HTTP ${http_code}" +grep -qF 'wordpress_logged_in_' "${jar}" || fail 'logging in did not set an authentication cookie' +echo 'ok login' + +# The admin bar renders only for a logged-in user. +check '/wp-admin/' 'id="wpadminbar"' '' + +# Catches a fatal during shutdown. An empty log would let the grep pass silently. +[ -s "${server_log}" ] || { echo '::error::the server wrote no log'; exit 1; } +if grep -qE 'Fatal error|Uncaught|Segmentation fault' "${server_log}"; then + echo '::error::the server logged a fatal error' + exit 1 +fi +echo 'ok server log' diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index d579368eeb079..ad099ad58460a 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -14,20 +14,61 @@ on: - '3.[89]' - '[4-9].[0-9]' paths: - # Any change to a PHP or JavaScript file should run checks. - - '**.js' - - '**.php' - # These files configure npm. Changes could affect the outcome. + - '**' + # Documentation, repository metadata, and local environment configuration. + - '!**.md' + - '!.devcontainer/**' + - '!.editorconfig' + - '!.env.example' + - '!.git-blame-ignore-revs' + - '!.gitignore' + - '!.mailmap' + - '!docker-compose.yml' + - '!jsdoc.conf.json' + - '!tsconfig.json' + - '!typings/**' + - '!wp-cli.yml' + # File types that PHPCS and JSHint do not inspect. + - '!**.crt' + - '!**.css' + - '!**.dat' + - '!**.dist' + - '!**.html' + - '!**.jpg' + - '!**.json' + - '!**.map' + - '!**.mo' + - '!**.neon' + - '!**.pem' + - '!**.png' + - '!**.po' + - '!**.scss' + - '!**.svg' + - '!**.txt' + - '!**.webp' + - '!**.woff2' + - '!**.xml' + - '!**.yaml' + - '!**.yml' + # JavaScript outside the JSHint targets and all tooling are not inspected. + - '!tests/e2e/**.js' + - '!tests/performance/**.js' + - '!tools/**' + - '!.eslintrc-jsdoc.js' + - '!.prettierrc.js' + - '!eslint.config.js' + - '!webpack.config.js' + # Other workflows do not affect these checks. + - '!.github/**' + # Re-include configuration and workflow files that affect these checks. - 'package*.json' - '.npmrc' - '.nvmrc' - # These files configure Composer. Changes could affect the outcome. + - 'Gruntfile.js' - 'composer.*' - # This file configures JSHint. Changes could affect the outcome. - - '.jshintrc' - # This file configures PHPCS. Changes could affect the outcome. + - '**.jshintrc' - 'phpcs.xml.dist' - # Confirm any changes to relevant workflow files. + - '.github/actions/setup-npm/**' - '.github/workflows/coding-standards.yml' - '.github/workflows/reusable-coding-standards-*.yml' workflow_dispatch: @@ -89,7 +130,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml index f2eb5abb83ddb..e249f6e25475c 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/end-to-end-tests.yml @@ -13,24 +13,37 @@ on: - '5.[3-9]' - '[6-9].[0-9]' paths: - # Any change to a PHP, CSS, or JavaScript file should run checks. - - '**.css' - - '**.js' - - '**.php' - # These files configure npm and the task runner. Changes could affect the outcome. - - 'package*.json' - - '.npmrc' - - '.nvmrc' - - 'Gruntfile.js' - - 'webpack.config.js' + - '**' + # Documentation and repository metadata. + - '!**.md' + - '!.devcontainer/**' + - '!.editorconfig' + - '!.git-blame-ignore-revs' + - '!.gitignore' + - '!.mailmap' + - '!typings/**' + # Configuration for unrelated checks. + - '!.eslintrc-jsdoc.js' + - '!.prettierrc.js' + - '!.version-support-*.json' + - '!eslint.config.js' + - '!jsdoc.conf.json' + - '!phpcompat.xml.dist' + - '!phpcs.xml.dist' + - '!phpstan.neon.dist' + - '!phpunit.xml.dist' + - '!tsconfig.json' + # Other test suites, tools, and workflows do not affect these tests. + - '!tests/**' + - '!tools/**' + - '!.github/**' + # Re-include the test, tool, and workflow files used here. + - 'tests/e2e/**' - 'tools/gutenberg/**' + - 'tools/local-env/**' - 'tools/vendors/**' - 'tools/webpack/**' - # These files configure Composer. Changes could affect the outcome. - - 'composer.*' - # This files affect the e2e tests. Changes could affect the outcome. - - 'tests/e2e/**' - # Confirm any changes to relevant workflow files. + - '.github/actions/setup-npm/**' - '.github/workflows/end-to-end-tests.yml' - '.github/workflows/reusable-end-to-end-tests*.yml' workflow_dispatch: @@ -89,7 +102,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.github/workflows/failed-workflow.yml b/.github/workflows/failed-workflow.yml index 288a2a9e0e930..2da0ff56989d7 100644 --- a/.github/workflows/failed-workflow.yml +++ b/.github/workflows/failed-workflow.yml @@ -43,9 +43,12 @@ jobs: // Only rerun after the first run attempt. if ( workflow_run.data.run_attempt > 1 ) { + core.info(`Skipping rerun, attempt ${workflow_run.data.run_attempt}: ${workflow_run.data.html_url}`); return; } + core.notice(`Rerunning workflow run "${workflow_run.data.name}" on ${workflow_run.data.head_branch}: ${workflow_run.data.html_url}`); + const rerun = await github.rest.actions.reRunWorkflowFailedJobs({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/.github/workflows/install-testing.yml b/.github/workflows/install-testing.yml index d69dc103d2d3a..ed0446e4e083e 100644 --- a/.github/workflows/install-testing.yml +++ b/.github/workflows/install-testing.yml @@ -12,12 +12,14 @@ on: - trunk # Always test the workflow after it's updated. paths: + - '.github/actions/smoke-check/**' - '.github/workflows/install-testing.yml' - '.version-support-*.json' - '.github/workflows/reusable-support-json-reader-v1.yml' pull_request: # Always test the workflow when changes are suggested. paths: + - '.github/actions/smoke-check/**' - '.version-support-*.json' - '.github/workflows/install-testing.yml' - '.github/workflows/reusable-support-json-reader-v1.yml' @@ -60,14 +62,19 @@ jobs: # - Downloads the specified version of WordPress. # - Creates a `wp-config.php` file. # - Installs WordPress. + # - Serves the installed site and confirms it renders. install-tests-mysql: name: WP ${{ inputs.wp-version || 'nightly' }} / PHP ${{ matrix.php }} / ${{ 'mariadb' == matrix.db-type && 'MariaDB' || 'MySQL' }} ${{ matrix.db-version }}${{ matrix.multisite && ' multisite' || '' }} permissions: contents: read runs-on: ${{ matrix.os }} if: ${{ github.repository == 'WordPress/wordpress-develop' }} - timeout-minutes: 10 + timeout-minutes: 15 needs: [ build-test-matrix ] + + env: + WP_ADMIN_USER: admin + WP_ADMIN_PASSWORD: password strategy: fail-fast: false matrix: @@ -106,8 +113,10 @@ jobs: image: ${{ matrix.db-type }}:${{ matrix.db-version }} ports: - 3306 + # Ping over TCP, to wait for the TCP server rather than the Unix socket. + # MariaDB 11.0 dropped the `mysqladmin` symlink, so pick the client the image ships. options: >- - --health-cmd="mysqladmin ping" + --health-cmd="${{ matrix.db-type == 'mariadb' && ! contains( fromJSON('["5.5", "10.0", "10.1", "10.2", "10.3"]'), matrix.db-version ) && 'mariadb-admin' || 'mysqladmin' }} ping -h 127.0.0.1 --protocol=tcp" --health-interval="30s" --health-timeout="10s" --health-retries="5" @@ -135,7 +144,18 @@ jobs: DB_PORT: ${{ job.services.database.ports['3306'] }} - name: Install WordPress - run: wp core ${{ matrix.multisite && 'multisite-install' || 'install' }} --url=http://localhost/ --title="Upgrade Test" --admin_user=admin --admin_password=password --admin_email=me@example.org --skip-email + run: | + wp core ${{ matrix.multisite && 'multisite-install' || 'install' }} \ + --url=http://localhost/ --title="Install Test" --admin_user="${WP_ADMIN_USER}" \ + --admin_password="${WP_ADMIN_PASSWORD}" --admin_email=me@example.org --skip-email + + - name: Post-install smoke check + # A network's domain lives in wp-config.php and the database, not just an option. + if: ${{ ! matrix.multisite }} + uses: WordPress/wordpress-develop/.github/actions/smoke-check@trunk # zizmor: ignore[unpinned-uses] + with: + admin-user: ${{ env.WP_ADMIN_USER }} + admin-password: ${{ env.WP_ADMIN_PASSWORD }} slack-notifications: name: Slack Notifications @@ -152,7 +172,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.github/workflows/javascript-tests.yml b/.github/workflows/javascript-tests.yml index 08a118e4a4056..97c6f1099a43e 100644 --- a/.github/workflows/javascript-tests.yml +++ b/.github/workflows/javascript-tests.yml @@ -13,9 +13,52 @@ on: - '3.[89]' - '[4-9].[0-9]' paths: - # Any change to a JavaScript file should run tests. - - '**.js' - # These files configure npm and the task runner. Changes could affect the outcome. + - '**' + # Documentation, repository metadata, and local environment configuration. + - '!**.md' + - '!.devcontainer/**' + - '!.editorconfig' + - '!.env.example' + - '!.git-blame-ignore-revs' + - '!.gitignore' + - '!.mailmap' + - '!docker-compose.yml' + - '!jsdoc.conf.json' + - '!typings/**' + - '!wp-cli.yml' + # File types that the JavaScript tests do not exercise. + - '!**.crt' + - '!**.css' + - '!**.dat' + - '!**.dist' + - '!**.html' + - '!**.jpg' + - '!**.json' + - '!**.map' + - '!**.mo' + - '!**.neon' + - '!**.pem' + - '!**.php' + - '!**.png' + - '!**.po' + - '!**.scss' + - '!**.svg' + - '!**.txt' + - '!**.webp' + - '!**.woff2' + - '!**.xml' + - '!**.yaml' + - '!**.yml' + # JavaScript outside the QUnit suite and related tooling is not exercised. + - '!tests/e2e/**.js' + - '!tests/performance/**.js' + - '!tools/**' + - '!.eslintrc-jsdoc.js' + - '!.prettierrc.js' + - '!eslint.config.js' + # Other workflows do not affect these tests. + - '!.github/**' + # Re-include configuration, tests, tools, and workflows used here. - 'package*.json' - '.npmrc' - '.nvmrc' @@ -24,13 +67,9 @@ on: - 'tools/gutenberg/**' - 'tools/vendors/**' - 'tools/webpack/**' - # This file configures ESLint. Changes could affect the outcome. - - '.eslintignore' - # This file configures JSHint. Changes could affect the outcome. - '.jshintrc' - # Any change to the QUnit directory should run tests. - 'tests/qunit/**' - # Confirm any changes to relevant workflow files. + - '.github/actions/setup-npm/**' - '.github/workflows/javascript-tests.yml' - '.github/workflows/reusable-javascript-tests.yml' workflow_dispatch: @@ -77,7 +116,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.github/workflows/javascript-type-checking.yml b/.github/workflows/javascript-type-checking.yml index 4983e0c619ec8..0bf9288b06e82 100644 --- a/.github/workflows/javascript-type-checking.yml +++ b/.github/workflows/javascript-type-checking.yml @@ -24,6 +24,7 @@ on: # This directory contains TypeScript definitions. Changes could affect the outcome. - 'typings/**' # Confirm any changes to relevant workflow files. + - '.github/actions/setup-npm/**' - '.github/workflows/javascript-type-checking.yml' - '.github/workflows/reusable-javascript-type-checking-v1.yml' workflow_dispatch: @@ -70,7 +71,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.github/workflows/local-docker-environment.yml b/.github/workflows/local-docker-environment.yml index 0f88d54ff4eb8..c636c695b6d44 100644 --- a/.github/workflows/local-docker-environment.yml +++ b/.github/workflows/local-docker-environment.yml @@ -13,6 +13,7 @@ on: - 'docker-compose.yml' # Any changes to local environment related files - 'tools/local-env/**' + - 'wp-cli.yml' # These files configure npm and the task runner. Changes could affect the outcome. - 'package*.json' - 'Gruntfile.js' @@ -27,6 +28,7 @@ on: # These files define the versions to test. - '.version-support-*.json' # Changes to this and related workflow files should always be verified. + - '.github/actions/setup-npm/**' - '.github/workflows/local-docker-environment.yml' - '.github/workflows/reusable-support-json-reader-v1.yml' - '.github/workflows/reusable-test-docker-environment-v1.yml' @@ -41,6 +43,7 @@ on: - 'docker-compose.yml' # Any changes to local environment related files - 'tools/local-env/**' + - 'wp-cli.yml' # These files configure npm and the task runner. Changes could affect the outcome. - 'package*.json' - 'Gruntfile.js' @@ -53,6 +56,7 @@ on: # These files define the versions to test. - '.version-support-*.json' # Changes to this and related workflow files should always be verified. + - '.github/actions/setup-npm/**' - '.github/workflows/local-docker-environment.yml' - '.github/workflows/reusable-support-json-reader-v1.yml' - '.github/workflows/reusable-test-docker-environment-v1.yml' @@ -135,7 +139,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 22a3a27392859..808f299208bc6 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -13,24 +13,37 @@ on: - '6.[2-9]' - '[7-9].[0-9]' paths: - # Any change to a PHP, CSS, or JavaScript file should run checks. - - '**.css' - - '**.js' - - '**.php' - # These files configure npm and the task runner. Changes could affect the outcome. - - 'package*.json' - - '.npmrc' - - '.nvmrc' - - 'Gruntfile.js' - - 'webpack.config.js' + - '**' + # Documentation and repository metadata. + - '!**.md' + - '!.devcontainer/**' + - '!.editorconfig' + - '!.git-blame-ignore-revs' + - '!.gitignore' + - '!.mailmap' + - '!typings/**' + # Configuration for unrelated checks. + - '!.eslintrc-jsdoc.js' + - '!.prettierrc.js' + - '!.version-support-*.json' + - '!eslint.config.js' + - '!jsdoc.conf.json' + - '!phpcompat.xml.dist' + - '!phpcs.xml.dist' + - '!phpstan.neon.dist' + - '!phpunit.xml.dist' + - '!tsconfig.json' + # Other test suites, tools, and workflows do not affect these tests. + - '!tests/**' + - '!tools/**' + - '!.github/**' + # Re-include the test, tool, and workflow files used here. + - 'tests/performance/**' - 'tools/gutenberg/**' + - 'tools/local-env/**' - 'tools/vendors/**' - 'tools/webpack/**' - # These files configure Composer. Changes could affect the outcome. - - 'composer.*' - # This files affect the performance tests. Changes could affect the outcome. - - 'tests/performance/**' - # Confirm any changes to relevant workflow files. + - '.github/actions/setup-npm/**' - '.github/workflows/performance.yml' - '.github/workflows/reusable-performance-*.yml' workflow_dispatch: @@ -153,7 +166,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.github/workflows/php-compatibility.yml b/.github/workflows/php-compatibility.yml index 37de91a73b24b..7bb6869fd5a79 100644 --- a/.github/workflows/php-compatibility.yml +++ b/.github/workflows/php-compatibility.yml @@ -13,13 +13,51 @@ on: - '5.[5-9]' - '[6-9].[0-9]' paths: - # This workflow only scans PHP files. - - '**.php' - # These files configure Composer. Changes could affect the outcome. + - '**' + # Documentation, repository metadata, and local environment configuration. + - '!**.md' + - '!.devcontainer/**' + - '!.editorconfig' + - '!.env.example' + - '!.git-blame-ignore-revs' + - '!.gitignore' + - '!.jshintrc' + - '!.mailmap' + - '!docker-compose.yml' + - '!jsdoc.conf.json' + - '!typings/**' + - '!wp-cli.yml' + # File types that the PHP compatibility scan does not inspect. + - '!**.crt' + - '!**.css' + - '!**.dat' + - '!**.dist' + - '!**.html' + - '!**.jpg' + - '!**.js' + - '!**.json' + - '!**.map' + - '!**.mo' + - '!**.neon' + - '!**.pem' + - '!**.png' + - '!**.po' + - '!**.scss' + - '!**.svg' + - '!**.txt' + - '!**.webp' + - '!**.woff2' + - '!**.xml' + - '!**.yaml' + - '!**.yml' + # Only source PHP files are scanned. + - '!*.php' + - '!tests/**' + - '!tools/**' + - '!.github/**' + # Re-include configuration and workflow files that affect the scan. - 'composer.*' - # This file configures PHP compatibility scanning. Changes could affect the outcome. - 'phpcompat.xml.dist' - # Confirm any changes to relevant workflow files. - '.github/workflows/php-compatibility.yml' - '.github/workflows/reusable-php-compatibility.yml' workflow_dispatch: @@ -66,7 +104,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.github/workflows/phpstan-pull-request-comments.yml b/.github/workflows/phpstan-pull-request-comments.yml new file mode 100644 index 0000000000000..aabe26def6414 --- /dev/null +++ b/.github/workflows/phpstan-pull-request-comments.yml @@ -0,0 +1,370 @@ +# Keeps a comment on a pull request up to date with the errors reported by PHPStan. +# +# The analysis runs in the PHPStan Static Analysis workflow, which has no permission to comment when the pull request +# comes from a fork. It uploads its results as an artifact instead, and this workflow picks them up once that run +# completes. +# +# A `workflow_run` workflow runs with write permissions even for a pull request from a fork, so nothing from the pull +# request is checked out or executed here. The results it uploaded are treated as untrusted data, and are only +# accepted for the pull request whose current head is the commit that was analyzed. +name: PHPStan Pull Request Comments + +on: # zizmor: ignore[dangerous-triggers] + workflow_run: + workflows: [ 'PHPStan Static Analysis' ] + types: + - completed + +# Runs for the same pull request are queued rather than run at the same time, so that two of them do not each create +# a comment. A queued run that is superseded by a newer one is cancelled, which is fine: only the newest results matter. +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }} + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Creates, updates, or resolves the comment listing PHPStan errors. + # + # Performs the following steps: + # - Downloads the analysis results from the completed run. + # - Creates or updates the comment while there are errors, and marks it as resolved once there are none. + phpstan-results: + name: Comment on a pull request with PHPStan results + runs-on: ubuntu-24.04 + permissions: + actions: read + issues: write + pull-requests: write + timeout-minutes: 5 + # A run that found errors concludes in failure only when some of them cannot be reported as warnings, and those + # are the errors this comment matters most for, so both conclusions are handled. + if: > + github.repository == 'WordPress/wordpress-develop' && + github.event.workflow_run.event == 'pull_request' && + ( github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure' ) + steps: + # The artifact is missing when the analysis did not complete, in which case there is nothing to report and the + # comment is left as it is. + - name: Download the analysis results + id: download + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + continue-on-error: true + with: + name: phpstan-results + path: ${{ runner.temp }}/phpstan-results + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Create, update, or resolve the PHPStan comment + if: ${{ steps.download.outcome == 'success' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const fs = require( 'fs' ); + + const { owner, repo } = context.repo; + const run = context.payload.workflow_run; + const marker = ''; + const maxRows = 50; + const readmeUrl = `https://github.com/${ owner }/${ repo }/blob/trunk/tests/phpstan/README.md`; + + // The results were written by a run on the pull request, whose code may have been changed to write + // anything at all. They are therefore checked for shape, and only accepted for the pull request whose + // current head is the commit that the run analyzed. That also discards the results of a run that was + // overtaken by a newer push. + const results = JSON.parse( fs.readFileSync( `${ process.env.RUNNER_TEMP }/phpstan-results/results.json`, 'utf8' ) ); + const issue_number = results.pull_request; + if ( ! Number.isSafeInteger( issue_number ) || issue_number <= 0 || ! Array.isArray( results.errors ) ) { + core.setFailed( 'The analysis results are malformed.' ); + return; + } + + const { data: pr } = await github.rest.pulls.get( { owner, repo, pull_number: issue_number } ); + if ( pr.head.sha !== run.head_sha ) { + core.info( `The results are not for the current head of pull request #${ issue_number }, so they are not reported.` ); + return; + } + if ( pr.state !== 'open' ) { + core.info( `Pull request #${ issue_number } is not open, so the results are not reported.` ); + return; + } + + const asString = ( value ) => ( typeof value === 'string' ? value : null ); + const asSha = ( value ) => ( typeof value === 'string' && /^[0-9a-f]{40}$/.test( value ) ? value : null ); + const normalize = ( error ) => ( { + file: asString( error.file ), + line: Number.isSafeInteger( error.line ) && error.line > 0 ? error.line : null, + message: ( asString( error.message ) ?? '' ).slice( 0, 1000 ), + identifier: /^[A-Za-z0-9.]+$/.test( error.identifier ) ? error.identifier : null, + ignorable: error.ignorable === true, + } ); + + // An error is recognized from one run to the next by its file, identifier and message, but not its line, + // which moves whenever lines above it are added or removed. The same error can occur more than once in a + // file, so each occurrence is told apart by its position among the others, in line order. When one of them + // is fixed or added, those after it shift to the history of the one before or after, so their first reports + // can be off, and the one shown as resolved or new is the last rather than the one that was. That is rarer + // than lines being added or removed above them, which would throw off matching them by line. + const errors = results.errors.map( normalize ); + const occurrences = new Map(); + for ( const error of [ ...errors ].sort( ( a, b ) => ( a.line ?? 0 ) - ( b.line ?? 0 ) ) ) { + const key = JSON.stringify( [ error.file, error.identifier, error.message ] ); + const occurrence = occurrences.get( key ) ?? 0; + occurrences.set( key, occurrence + 1 ); + error.key = `${ key }#${ occurrence }`; + } + + const comments = await github.paginate( github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 } ); + const comment = comments.find( ( item ) => item.user?.login === 'github-actions[bot]' && item.body?.includes( marker ) ); + + // The history of the errors is kept in the comment itself, compressed into a hidden HTML comment: each + // error, the commit it was first reported for, the last commit it was reported for, and the commit that + // resolved it, if one has. Base64 cannot contain `--`, so it cannot end the HTML comment early. + const zlib = require( 'zlib' ); + const historyPattern = //; + let history = []; + const historyMatch = comment?.body?.match( historyPattern ); + if ( historyMatch ) { + try { + const parsed = JSON.parse( zlib.inflateRawSync( Buffer.from( historyMatch[ 1 ], 'base64' ) ).toString( 'utf8' ) ); + history = ( Array.isArray( parsed ) ? parsed : [] ) + .map( ( entry ) => ( { + ...normalize( entry ), + key: asString( entry.key ), + introduced: asSha( entry.introduced ), + seen: asSha( entry.seen ), + fixed: asSha( entry.fixed ), + } ) ) + .filter( ( entry ) => entry.key && entry.introduced && entry.seen ); + } catch ( error ) { + core.warning( `The error history in the PHPStan comment could not be read, so it starts over: ${ error.message }` ); + } + } + + // An error that was already being reported keeps the commit it was first reported for. One that is new, + // or that comes back after being resolved, is attributed to this commit. One that is no longer reported + // is attributed as resolved by this commit, unless an earlier commit already resolved it. + const historyByKey = new Map( history.map( ( entry ) => [ entry.key, entry ] ) ); + const active = errors.map( ( error ) => { + const before = historyByKey.get( error.key ); + return { ...error, introduced: before && ! before.fixed ? before.introduced : run.head_sha, seen: run.head_sha, fixed: null }; + } ); + const activeKeys = new Set( active.map( ( error ) => error.key ) ); + let resolved = [ + ...history.filter( ( entry ) => entry.fixed && ! activeKeys.has( entry.key ) ), + ...history.filter( ( entry ) => ! entry.fixed && ! activeKeys.has( entry.key ) ).map( ( entry ) => ( { ...entry, fixed: run.head_sha } ) ), + ].slice( -maxRows ); + + if ( ! comment && active.length === 0 ) { + core.info( 'PHPStan reported no errors, and there is no comment to update.' ); + return; + } + + // Files the base branch has changed since the pull request branched from it, in which the line numbers + // reported can be off in the head commit, since the analysis ran on the head commit merged into the base. + const changedInBase = new Set( Array.isArray( results.changed_in_base ) ? results.changed_in_base.filter( ( file ) => typeof file === 'string' ) : [] ); + const lineMayBeOff = ( error ) => ! error.fixed && Boolean( error.line ) && changedInBase.has( error.file ); + const filesWithLinesOff = new Set( active.filter( lineMayBeOff ).map( ( error ) => error.file ) ); + + // Every message is rendered as a code span. Within one, Markdown is not interpreted, and neither are + // mentions nor references to issues, both of which PHPStan messages can resemble, as in `Parameter #1`. + // In a table, a pipe still ends the cell even within a code span, unless it is escaped. + const code = ( text ) => { + let content = text.replace( /\s+/g, ' ' ).trim(); + if ( content.length > 500 ) { + content = `${ content.slice( 0, 500 ) }…`; + } + content = content.replace( /\|/g, '\\|' ); + const longestBackticks = Math.max( 0, ...( content.match( /`+/g ) ?? [] ).map( ( backticks ) => backticks.length ) ); + const fence = '`'.repeat( longestBackticks + 1 ); + const padding = content.startsWith( '`' ) || content.endsWith( '`' ) ? ' ' : ''; + return `${ fence }${ padding }${ content }${ padding }${ fence }`; + }; + + // A bare URL to a commit is shortened by GitHub to its abbreviated hash, with a hovercard for the commit. + const commit = ( sha ) => `https://github.com/${ owner }/${ repo }/commit/${ sha }`; + + // A location links to its line in the last commit the error was reported for, rather than to the merge + // commit that was analyzed, which is replaced whenever either side changes, so links to it would not last. + // Where the base branch has changed the file, the line may be off, so only the file is linked. A resolved + // error is linked to its line as the place it was last reported, unless that line was off, in which case + // its line is not kept in the history. + const location = ( error ) => { + if ( ! error.file ) { + return '_Not specific to a file_'; + } + const label = code( error.line ? `${ error.file }:${ error.line }` : error.file ); + if ( ! /^[\w.\/-]+$/.test( error.file ) || error.file.split( '/' ).includes( '..' ) ) { + return label; + } + return `[${ label }](https://github.com/${ owner }/${ repo }/blob/${ error.seen }/${ error.file }${ error.line && ! lineMayBeOff( error ) ? `#L${ error.line }` : '' })`; + }; + + const identifier = ( error ) => ( error.identifier ? `[${ code( error.identifier ) }](https://phpstan.org/error-identifiers/${ error.identifier })` : '' ); + + // GitHub shows only the first few annotations of each kind from a step on the page of the workflow run, so + // errors left out of a table are pointed to in the log of the step that reported them, which has them all. + const where = `the log of the [workflow run](${ run.html_url }), in the “Report the analysis results” step`; + const table = ( rows, rowLimit, { withResolution = false } = {} ) => { + const shown = rows.slice( 0, rowLimit ); + if ( shown.length === 0 ) { + return rows.length === 1 + ? `The error is not listed here, to keep this comment short. It is listed in ${ where }.` + : `The ${ rows.length } errors are not listed here, to keep this comment short. They are listed in ${ where }.`; + } + const more = `…and ${ rows.length - shown.length } more, which are listed in ${ where }.`; + const lines = [ + `| Location | Error | Identifier | First reported |${ withResolution ? ' Resolved |' : '' }`, + `| --- | --- | --- | --- |${ withResolution ? ' --- |' : '' }`, + ...shown.map( ( error ) => `| ${ location( error ) } | ${ code( error.message ) } | ${ identifier( error ) } | ${ commit( error.introduced ) } |${ withResolution ? ` ${ commit( error.fixed ) } |` : '' }` ), + ]; + if ( shown.length < rows.length ) { + lines.push( '', more ); + } + return lines.join( '\n' ); + }; + + const unignorable = active.filter( ( error ) => ! error.ignorable ); + const ignorable = active.filter( ( error ) => error.ignorable ); + const hasIgnoreErrors = unignorable.some( ( error ) => error.identifier?.startsWith( 'ignore.' ) ); + + const render = ( trackedLimit ) => { + // The heading shows at a glance whether the check fails, has only warnings, or has nothing left to report. + let status = '✅'; + if ( unignorable.length > 0 ) { + status = '❌'; + } else if ( active.length > 0 ) { + status = '⚠️'; + } + const sections = [ marker, `## ${ status } PHPStan Static Analysis` ]; + + if ( active.length > 0 ) { + sections.push( `PHPStan reported ${ active.length === 1 ? '1 error' : `${ active.length } errors` } when analyzing ${ commit( run.head_sha ) } ([workflow run](${ run.html_url })). This comment is updated as new commits are pushed.` ); + if ( filesWithLinesOff.size > 0 ) { + sections.push( [ + '> [!NOTE]', + `> ${ filesWithLinesOff.size === 1 ? 'A file with errors has' : `${ filesWithLinesOff.size } files with errors have` } changed in ${ code( pr.base.ref ) } since this branch was last updated, so the line numbers reported in ${ filesWithLinesOff.size === 1 ? 'it' : 'them' } may be off and are not linked. Merge the latest ${ code( pr.base.ref ) } into this branch for accurate links.`, + ].join( '\n' ) ); + } + } else { + // Resolved by the last commit that resolved anything, so that later commits do not change the comment. + const resolvedBy = resolved.at( -1 )?.fixed ?? run.head_sha; + sections.push( `All errors reported by PHPStan were resolved as of ${ commit( resolvedBy ) }.` ); + } + + let rowsLeft = Math.min( maxRows, trackedLimit ); + if ( unignorable.length > 0 ) { + if ( hasIgnoreErrors ) { + sections.push( [ + '> [!IMPORTANT]', + '> **The PHPStan baselines are out of date, so the PHPStan check fails.**', + '>', + '> An error recorded in a baseline no longer occurs, or occurs a different number of times. If this pull request fixed it, that is expected: nothing needs to change in the code, but each baseline entry records an exact count for a specific file, and the baselines need to catch up. Regenerate them and commit the result:', + '>', + '> ```bash', + '> npm run typecheck:php:baselines', + '> ```', + '>', + '> or, outside the Docker environment:', + '>', + '> ```bash', + '> composer phpstan:baselines', + '> ```', + '>', + '> Where an error below names an `@phpstan-ignore` comment in the code rather than a baseline entry, remove that comment instead; regenerating will not clear it.', + '>', + `> See [Regenerating the baselines](${ readmeUrl }#regenerating-the-baselines) for details.`, + ].join( '\n' ) ); + } else { + sections.push( [ + '> [!IMPORTANT]', + '> **PHPStan reported errors that cannot be ignored, so the PHPStan check fails.**', + ].join( '\n' ) ); + } + sections.push( '### Errors that fail the check', table( unignorable, rowsLeft ) ); + rowsLeft = Math.max( 0, rowsLeft - unignorable.length ); + } + + if ( ignorable.length > 0 ) { + sections.push( + '### Errors', + `These are also shown as annotations on the changed files. They do not fail the check, but they should still be fixed before the pull request is committed. An error that is a false positive may instead warrant being [added to a baseline](${ readmeUrl }#ignoring-and-baselining-errors).`, + table( ignorable, rowsLeft ) + ); + } + + // Most recently resolved first. + if ( resolved.length > 0 ) { + sections.push( [ + '
', + `Resolved errors (${ resolved.length })`, + '', + table( [ ...resolved ].reverse(), maxRows, { withResolution: true } ), + '', + '
', + ].join( '\n' ) ); + } + + if ( active.length > 0 ) { + sections.push( + '### Running PHPStan locally', + `To [run the analysis](${ readmeUrl }#running-the-tests) on your own machine, use \`npm run typecheck:php\`, or \`composer phpstan\` outside the Docker environment.` + ); + } + + // The errors tracked are taken in the order they are listed, so that each one listed keeps its history. + const tracked = [ ...unignorable, ...ignorable ].slice( 0, trackedLimit ).concat( resolved ).map( ( error ) => { + const { key, file, line, message, identifier, ignorable, introduced, seen, fixed } = error; + return { key, file, line: lineMayBeOff( error ) ? null : line, message, identifier, ignorable, introduced, seen, fixed }; + } ); + sections.push( `` ); + + return sections.join( '\n\n' ) + '\n'; + }; + + // A comment is limited to 65,536 characters. Should the body exceed that, give up the oldest resolved + // errors first, and then list and track fewer of the current errors. The history of current errors is + // given up last, even for errors that are not listed: an error whose history is lost is taken to be + // introduced by whichever later commit it is reported for, which would then be shown as its first report. + const maxLength = 65000; + let trackedLimit = 200; + let body = render( trackedLimit ); + while ( body.length > maxLength && resolved.length > 0 ) { + resolved = resolved.slice( 1 ); + body = render( trackedLimit ); + } + while ( body.length > maxLength && trackedLimit > 0 ) { + trackedLimit = Math.floor( trackedLimit / 2 ); + body = render( trackedLimit ); + } + + if ( ! comment ) { + await github.rest.issues.createComment( { owner, repo, issue_number, body } ); + core.notice( `Created PHPStan comment for pull request: ${ pr.html_url }` ); + return; + } + + if ( comment.body !== body ) { + await github.rest.issues.updateComment( { owner, repo, comment_id: comment.id, body } ); + core.notice( `Updated PHPStan comment for pull request: ${ pr.html_url }` ); + } + + // The comment is collapsed as resolved while there are no errors, and expanded again if errors return. + const { node } = await github.graphql( + 'query( $id: ID! ) { node( id: $id ) { ... on IssueComment { isMinimized } } }', + { id: comment.node_id } + ); + if ( active.length === 0 && ! node.isMinimized ) { + await github.graphql( + 'mutation( $id: ID! ) { minimizeComment( input: { subjectId: $id, classifier: RESOLVED } ) { clientMutationId } }', + { id: comment.node_id } + ); + core.notice( `Resolved PHPStan comment for pull request: ${ pr.html_url }` ); + } else if ( active.length > 0 && node.isMinimized ) { + await github.graphql( + 'mutation( $id: ID! ) { unminimizeComment( input: { subjectId: $id } ) { clientMutationId } }', + { id: comment.node_id } + ); + } diff --git a/.github/workflows/phpstan-static-analysis.yml b/.github/workflows/phpstan-static-analysis.yml index a07ee13e16467..6a26c75b16525 100644 --- a/.github/workflows/phpstan-static-analysis.yml +++ b/.github/workflows/phpstan-static-analysis.yml @@ -66,7 +66,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.github/workflows/phpunit-tests-full-matrix.yml b/.github/workflows/phpunit-tests-full-matrix.yml new file mode 100644 index 0000000000000..eedbbdb087e4e --- /dev/null +++ b/.github/workflows/phpunit-tests-full-matrix.yml @@ -0,0 +1,66 @@ +name: PHPUnit Tests (label) +run-name: PHPUnit Tests${{ github.event.label.name == 'Full PHPUnit Matrix' && ' (full matrix)' || ' (ignored label)' }} + +on: + pull_request: + types: [ labeled ] + branches: + - trunk + - '3.[7-9]' + - '[4-9].[0-9]' + # Keep these paths in sync with phpunit-tests.yml. + paths: + - '**' + # Documentation and repository metadata. + - '!**.md' + - '!.devcontainer/**' + - '!.editorconfig' + - '!.git-blame-ignore-revs' + - '!.gitignore' + - '!.mailmap' + - '!typings/**' + # Configuration for unrelated checks. + - '!.eslintrc-jsdoc.js' + - '!.prettierrc.js' + - '!.version-support-*.json' + - '!eslint.config.js' + - '!jsdoc.conf.json' + - '!phpcompat.xml.dist' + - '!phpcs.xml.dist' + - '!phpstan.neon.dist' + - '!tsconfig.json' + # Other test suites, tools, and workflows do not affect these tests. + - '!tests/**' + - '!tools/**' + - '!.github/**' + # Re-include documentation, tests, tools, and workflows used here. + - 'SECURITY.md' + - 'src/license.txt' + - 'tests/phpunit/**' + - 'tools/gutenberg/**' + - 'tools/local-env/**' + - 'tools/vendors/**' + - 'tools/webpack/**' + - 'phpunit.xml.dist' + - '.github/actions/setup-npm/**' + - '.github/workflows/phpunit-tests.yml' + - '.github/workflows/phpunit-tests-full-matrix.yml' + - '.github/workflows/reusable-phpunit-tests-*.yml' + - '.github/workflows/reusable-prepare-gutenberg.yml' + +# Disable permissions for all available scopes by default. +permissions: {} + +# The called workflow manages concurrency. Unrelated labels never enter it. +jobs: + full-matrix: + if: github.event.label.name == 'Full PHPUnit Matrix' + permissions: + # Required by the called workflow's recovery job, which is skipped on PR events. + actions: write + contents: read + uses: ./.github/workflows/phpunit-tests.yml + secrets: + CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + WPT_REPORT_API_KEY: ${{ secrets.WPT_REPORT_API_KEY }} diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index 30cf72bc606ae..a212e3b6be563 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -1,4 +1,5 @@ name: PHPUnit Tests +run-name: PHPUnit Tests${{ ( github.event_name == 'schedule' || ( github.event_name == 'pull_request' && contains( github.event.pull_request.labels.*.name, 'Full PHPUnit Matrix' ) ) ) && ' (full matrix)' || '' }} on: push: @@ -11,31 +12,62 @@ on: - trunk - '3.[7-9]' - '[4-9].[0-9]' + # Keep these paths in sync with phpunit-tests-full-matrix.yml. paths: - # Any change to a PHP, CSS, JavaScript, JSON, HTML, or otherwise tested file should run checks. - - '**.css' - - '**.html' - - '**.js' - - '**.json' - - '**.php' + - '**' + # Documentation and repository metadata. + - '!**.md' + - '!.devcontainer/**' + - '!.editorconfig' + - '!.git-blame-ignore-revs' + - '!.gitignore' + - '!.mailmap' + - '!typings/**' + # Configuration for unrelated checks. + - '!.eslintrc-jsdoc.js' + - '!.prettierrc.js' + - '!.version-support-*.json' + - '!eslint.config.js' + - '!jsdoc.conf.json' + - '!phpcompat.xml.dist' + - '!phpcs.xml.dist' + - '!phpstan.neon.dist' + - '!tsconfig.json' + # Other test suites, tools, and workflows do not affect these tests. + - '!tests/**' + - '!tools/**' + - '!.github/**' + # Re-include documentation, tests, tools, and workflows used here. + - 'SECURITY.md' - 'src/license.txt' - - 'src/SECURITY.md' - # These files configure npm and the task runner. Changes could affect the outcome. - - 'package*.json' - - '.npmrc' - - '.nvmrc' - - 'Gruntfile.js' - # These files configure Composer. Changes could affect the outcome. - - 'composer.*' - # These files affect the phpunit tests. Changes could affect the outcome. - 'tests/phpunit/**' - - 'tests/phpunit/multisite.xml' + - 'tools/gutenberg/**' + - 'tools/local-env/**' + - 'tools/vendors/**' + - 'tools/webpack/**' - 'phpunit.xml.dist' - # Confirm any changes to relevant workflow files. + - '.github/actions/setup-npm/**' - '.github/workflows/phpunit-tests.yml' + - '.github/workflows/phpunit-tests-full-matrix.yml' - '.github/workflows/reusable-phpunit-tests-*.yml' - '.github/workflows/reusable-prepare-gutenberg.yml' workflow_dispatch: + workflow_call: + secrets: + CODEVITALS_PROJECT_TOKEN: + required: false + CODECOV_TOKEN: + required: false + WPT_REPORT_API_KEY: + required: false + SLACK_GHA_SUCCESS_WEBHOOK: + required: false + SLACK_GHA_CANCELLED_WEBHOOK: + required: false + SLACK_GHA_FIXED_WEBHOOK: + required: false + SLACK_GHA_FAILURE_WEBHOOK: + required: false # Once weekly On Sundays at 00:00 UTC. schedule: - cron: '0 0 * * 0' @@ -52,6 +84,88 @@ concurrency: permissions: {} jobs: + # Reduce PR testing only for audited Core assets and unrelated companion files. + select-test-coverage: + name: Select PHPUnit coverage + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + if: | + github.repository == 'WordPress/wordpress-develop' || ( + github.event_name == 'pull_request' && ( + ! github.event.repository.private || + ! github.event.pull_request.draft || + contains( github.event.pull_request.labels.*.name, 'Draft Workflow Runs' ) + ) + ) + outputs: + assets-only: ${{ steps.select.outputs.assets-only }} + steps: + - name: Checkout the PR merge commit + if: github.event_name == 'pull_request' && ! contains( github.event.pull_request.labels.*.name, 'Full PHPUnit Matrix' ) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 2 + persist-credentials: false + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + + - name: Select test coverage + id: select + env: + CHECK_ASSETS: ${{ github.event_name == 'pull_request' && ! contains( github.event.pull_request.labels.*.name, 'Full PHPUnit Matrix' ) }} + run: | + assets_only=false + if [[ "$CHECK_ASSETS" == 'true' ]]; then + # Compare the tested merge result with its base parent. Include both paths of a rename. + changed_files="$RUNNER_TEMP/phpunit-changed-files" + if git diff --no-renames --name-only -z HEAD^1 HEAD > "$changed_files" && [[ -s "$changed_files" ]]; then + while IFS= read -r -d '' file; do + case "$file" in + # Preserve the workflow's explicit inclusions before ignoring Markdown files. + SECURITY.md|tests/phpunit/*|tools/gutenberg/*|tools/local-env/*|tools/vendors/*|tools/webpack/*) + ;; + src/js/*.js|src/js/*.css|src/wp-admin/css/*.css|src/wp-admin/css/colors/*.scss|src/wp-includes/css/*.css|src/wp-includes/build/*.js) + assets_only=true + continue + ;; + # These companion files are excluded by the workflow's path filters. + *.md|tests/qunit/*|typings/*|.eslintrc-jsdoc.js|.prettierrc.js|eslint.config.js|jsdoc.conf.json|tsconfig.json) + continue + ;; + esac + printf 'Running the normal PHPUnit matrix: %q is outside the audited asset paths.\n' "$file" + assets_only=false + break + done < "$changed_files" + if [[ "$assets_only" == 'false' ]]; then + echo 'Reduced coverage requires an audited asset change and no other relevant changes.' + fi + else + echo 'Running the normal PHPUnit matrix: changed paths are unavailable or empty.' + fi + else + echo 'Keeping the existing PHPUnit matrix for this event or the Full PHPUnit Matrix label.' + fi + echo "assets-only=$assets_only" >> "$GITHUB_OUTPUT" + + # One configuration keeps asset-dependent PHP tests visible on Core asset-only PRs. + # Single site only. Asset tests must not also belong to groups excluded by phpunit.xml.dist. + test-assets: + name: Core assets + uses: ./.github/workflows/reusable-phpunit-tests-v3.yml + needs: [ select-test-coverage, prepare-gutenberg ] + permissions: + contents: read + if: needs.select-test-coverage.outputs.assets-only == 'true' + with: + php: '8.3' + db-type: 'mysql' + db-version: '8.4' + phpunit-test-groups: 'assets' + gutenberg-artifact: ${{ needs.prepare-gutenberg.result != 'skipped' }} + gutenberg-sha: ${{ needs.prepare-gutenberg.outputs.gutenberg-sha }} + # Downloads and verifies the Gutenberg build once for all PHPUnit jobs. prepare-gutenberg: name: Prepare Gutenberg Assets @@ -77,13 +191,15 @@ jobs: test-with-mysql: name: PHP ${{ matrix.php }} uses: ./.github/workflows/reusable-phpunit-tests-v3.yml - needs: prepare-gutenberg + needs: [ select-test-coverage, prepare-gutenberg ] permissions: contents: read secrets: + CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} WPT_REPORT_API_KEY: ${{ secrets.WPT_REPORT_API_KEY }} if: | + needs.select-test-coverage.outputs.assets-only != 'true' && startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || ( github.event_name == 'pull_request' && ( @@ -97,13 +213,30 @@ jobs: fail-fast: false matrix: os: [ ubuntu-24.04 ] - # The scheduled run tests every supported PHP version. Other events test the highest and lowest of each major. - php: ${{ github.event_name == 'schedule' && fromJSON('["7.4","8.0","8.1","8.2","8.3","8.4","8.5"]') || fromJSON('["7.4","8.0","8.5"]') }} + php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] db-type: [ 'mysql' ] db-version: [ '5.7', '8.0', '8.4', '9.7' ] tests-domain: [ 'example.org' ] multisite: [ false, true ] memcached: [ false ] + # Scheduled runs and pull requests labeled with 'Full PHPUnit Matrix' test every PHP and MySQL version above. + # All other runs exclude the versions noted below to keep the number of jobs manageable. + full-matrix: + - ${{ github.event_name == 'schedule' || ( github.event_name == 'pull_request' && contains( github.event.pull_request.labels.*.name, 'Full PHPUnit Matrix' ) ) }} + + exclude: + # Outside the full matrix, only the highest and lowest PHP version of each major are tested. + - full-matrix: false + php: '8.1' + - full-matrix: false + php: '8.2' + - full-matrix: false + php: '8.3' + - full-matrix: false + php: '8.4' + # Outside the full matrix, MySQL 5.7 compatibility and the widely used 8.x releases are tested. + - full-matrix: false + db-version: '9.7' include: # Include jobs that test with memcached. @@ -160,8 +293,8 @@ jobs: # # Creates a PHPUnit test job for each PHP/MariaDB combination. # - # All LTS versions of MariaDB supported by WordPress with greater than 1% usage according to w.org/stats should be - # tested. The exceptions to this rule are the most recent LTS and version 5.5. + # Scheduled and labeled runs test all LTS versions of MariaDB supported by WordPress with greater than 1% usage + # according to w.org/stats. The exceptions to this rule are the most recent LTS and version 5.5. # # The 5.5 release was intended as a drop-in replacement for MySQL. Because the MySQL 5.5 Docker containers do not # work, this ensures some level of MySQL 5.5 testing. @@ -169,13 +302,14 @@ jobs: test-with-mariadb: name: PHP ${{ matrix.php }} uses: ./.github/workflows/reusable-phpunit-tests-v3.yml - needs: prepare-gutenberg + needs: [ select-test-coverage, prepare-gutenberg ] permissions: contents: read secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} WPT_REPORT_API_KEY: ${{ secrets.WPT_REPORT_API_KEY }} if: | + needs.select-test-coverage.outputs.assets-only != 'true' && startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || ( github.event_name == 'pull_request' && ( @@ -189,12 +323,31 @@ jobs: fail-fast: false matrix: os: [ ubuntu-24.04 ] - # The scheduled run tests every supported PHP version. Other events test the highest and lowest of each major. - php: ${{ github.event_name == 'schedule' && fromJSON('["7.4","8.0","8.1","8.2","8.3","8.4","8.5"]') || fromJSON('["7.4","8.0","8.5"]') }} + php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] db-type: [ 'mariadb' ] db-version: [ '5.5', '10.3', '10.5', '10.6', '10.11', '11.4', '11.8' ] multisite: [ false, true ] memcached: [ false ] + # Scheduled runs and pull requests labeled with 'Full PHPUnit Matrix' test every PHP and MariaDB version above. + # All other runs exclude the versions noted below to keep the number of jobs manageable. + full-matrix: + - ${{ github.event_name == 'schedule' || ( github.event_name == 'pull_request' && contains( github.event.pull_request.labels.*.name, 'Full PHPUnit Matrix' ) ) }} + + exclude: + # Outside the full matrix, only the highest and lowest PHP version of each major are tested. + - full-matrix: false + php: '8.1' + - full-matrix: false + php: '8.2' + - full-matrix: false + php: '8.3' + - full-matrix: false + php: '8.4' + # Outside the full matrix, MariaDB 5.5 compatibility, 10.6, and newer LTS releases are tested. + - full-matrix: false + db-version: '10.3' + - full-matrix: false + db-version: '10.5' include: # Include jobs that test with memcached. @@ -236,13 +389,14 @@ jobs: test-innovation-releases: name: PHP ${{ matrix.php }} uses: ./.github/workflows/reusable-phpunit-tests-v3.yml - needs: prepare-gutenberg + needs: [ select-test-coverage, prepare-gutenberg ] permissions: contents: read secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} WPT_REPORT_API_KEY: ${{ secrets.WPT_REPORT_API_KEY }} if: | + needs.select-test-coverage.outputs.assets-only != 'true' && startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || ( github.event_name == 'pull_request' && ( @@ -256,18 +410,30 @@ jobs: fail-fast: false matrix: os: [ ubuntu-24.04 ] - # The scheduled run tests every supported PHP version. Other events test the highest and lowest of each major. - php: ${{ github.event_name == 'schedule' && fromJSON('["7.4","8.0","8.1","8.2","8.3","8.4","8.5"]') || fromJSON('["7.4","8.0","8.5"]') }} + php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] db-type: [ 'mysql', 'mariadb' ] db-version: [ '12.1' ] multisite: [ false, true ] memcached: [ false ] db-innovation: [ true ] + # Scheduled runs and pull requests labeled with 'Full PHPUnit Matrix' test every version above. + # All other runs exclude the versions noted below to keep the number of jobs manageable. + full-matrix: + - ${{ github.event_name == 'schedule' || ( github.event_name == 'pull_request' && contains( github.event.pull_request.labels.*.name, 'Full PHPUnit Matrix' ) ) }} exclude: # Exclude version combinations that don't exist. - db-type: 'mysql' db-version: '12.1' + # Outside the full matrix, only the highest and lowest PHP version of each major are tested. + - full-matrix: false + php: '8.1' + - full-matrix: false + php: '8.2' + - full-matrix: false + php: '8.3' + - full-matrix: false + php: '8.4' with: os: ${{ matrix.os }} php: ${{ matrix.php }} @@ -292,13 +458,14 @@ jobs: html-api-test-groups: name: ${{ matrix.label }} uses: ./.github/workflows/reusable-phpunit-tests-v3.yml - needs: prepare-gutenberg + needs: [ select-test-coverage, prepare-gutenberg ] permissions: contents: read secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} WPT_REPORT_API_KEY: ${{ secrets.WPT_REPORT_API_KEY }} if: | + needs.select-test-coverage.outputs.assets-only != 'true' && startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || ( github.event_name == 'pull_request' && ( @@ -335,13 +502,14 @@ jobs: limited-matrix-for-forks: name: PHP ${{ matrix.php }} uses: ./.github/workflows/reusable-phpunit-tests-v3.yml - needs: prepare-gutenberg + needs: [ select-test-coverage, prepare-gutenberg ] permissions: contents: read secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} WPT_REPORT_API_KEY: ${{ secrets.WPT_REPORT_API_KEY }} if: | + needs.select-test-coverage.outputs.assets-only != 'true' && ! startsWith( github.repository, 'WordPress/' ) && github.event_name == 'pull_request' && ( ! github.event.repository.private || @@ -396,13 +564,57 @@ jobs: gutenberg-artifact: ${{ needs.prepare-gutenberg.result != 'skipped' }} gutenberg-sha: ${{ needs.prepare-gutenberg.outputs.gutenberg-sha }} + # Runs the PHPUnit test suite with PHP 8.6. + # + # The number of testing combinations is intentionally limited because PHP 8.6 is still in beta/RC. + pre-release-php-8-6: + name: PHP ${{ matrix.php }} + uses: ./.github/workflows/reusable-phpunit-tests-v3.yml + needs: [ select-test-coverage, prepare-gutenberg ] + permissions: + contents: read + if: | + needs.select-test-coverage.outputs.assets-only != 'true' && + startsWith( github.repository, 'WordPress/' ) && ( + github.repository == 'WordPress/wordpress-develop' || ( + github.event_name == 'pull_request' && ( + ! github.event.repository.private || + ! github.event.pull_request.draft || + contains( github.event.pull_request.labels.*.name, 'Draft Workflow Runs' ) + ) + ) + ) + strategy: + fail-fast: false + matrix: + php: [ '8.6' ] + db-version: [ '9.7', '11.8' ] + db-type: [ 'mysql', 'mariadb' ] + multisite: [ true, false ] + + exclude: + - db-type: 'mysql' + db-version: '11.8' + - db-type: 'mariadb' + db-version: '9.7' + + with: + php: ${{ matrix.php }} + db-version: ${{ matrix.db-version }} + db-type: ${{ matrix.db-type }} + multisite: ${{ matrix.multisite }} + phpunit-config: ${{ matrix.multisite && 'tests/phpunit/multisite.xml' || 'phpunit.xml.dist' }} + allow-errors: true + gutenberg-artifact: ${{ needs.prepare-gutenberg.result != 'skipped' }} + gutenberg-sha: ${{ needs.prepare-gutenberg.outputs.gutenberg-sha }} + slack-notifications: name: Slack Notifications uses: ./.github/workflows/slack-notifications.yml permissions: actions: read contents: read - needs: [ prepare-gutenberg, test-with-mysql, test-with-mariadb, test-innovation-releases, html-api-test-groups, limited-matrix-for-forks ] + needs: [ select-test-coverage, prepare-gutenberg, test-assets, test-with-mysql, test-with-mariadb, test-innovation-releases, html-api-test-groups, limited-matrix-for-forks, pre-release-php-8-6 ] if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} with: calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} @@ -411,7 +623,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.github/workflows/props-bot.yml b/.github/workflows/props-bot.yml index 38acc07113c2c..eea736f5c4ea3 100644 --- a/.github/workflows/props-bot.yml +++ b/.github/workflows/props-bot.yml @@ -70,6 +70,11 @@ jobs: ( ! github.event.pull_request.draft && github.event.pull_request.state == 'open' || ! github.event.issue.draft && github.event.issue.state == 'open' ) steps: + - name: Output PR link + env: + PR_URL: ${{ github.event.pull_request.html_url || github.event.issue.html_url }} + run: 'echo "::notice::Pull request: $PR_URL"' + - name: Gather a list of contributors uses: WordPress/props-bot-action@trunk with: diff --git a/.github/workflows/pull-request-comments.yml b/.github/workflows/pull-request-comments.yml index 8dff8e1c1fcce..6c56326774b8d 100644 --- a/.github/workflows/pull-request-comments.yml +++ b/.github/workflows/pull-request-comments.yml @@ -121,7 +121,8 @@ jobs: [Test this pull request with WordPress Playground](https://playground.wordpress.net/wordpress.html?pr=${ issue_number }). `; - github.rest.issues.createComment( commentInfo ); + await github.rest.issues.createComment( commentInfo ); + core.notice( `Created Playground comment for pull request: ${ pr.data.html_url }` ); # Manages comments reminding contributors to include a Trac ticket link when opening a pull request. trac-ticket-check: @@ -160,7 +161,8 @@ jobs: const tracTicketMatches = prBody.match( tracTicketRegex ) || prTitle.match( tracTicketRegex ); if ( ! tracTicketMatches ) { - github.rest.issues.createComment( { + core.info( `Creating missing Trac ticket comment for pull request: ${ pr.html_url }` ); + await github.rest.issues.createComment( { owner, repo, issue_number: number, diff --git a/.github/workflows/reusable-build-package.yml b/.github/workflows/reusable-build-package.yml index 5005b2149890c..4ca4b6e87dcb4 100644 --- a/.github/workflows/reusable-build-package.yml +++ b/.github/workflows/reusable-build-package.yml @@ -1,5 +1,7 @@ ## # A reusable workflow that builds and packages WordPress. The resulting package can be used to test upgrading and installing. +# +# This workflow is used by `trunk` and branches >= 6.8. ## name: Build and package WordPress @@ -38,12 +40,18 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: '.nvmrc' - cache: npm + package-manager-cache: false + + - name: Set up npm + # Release branches run this workflow from trunk, but some older branches may lack the action. + if: ${{ hashFiles('.github/actions/setup-npm/action.yml') != '' }} + uses: ./.github/actions/setup-npm - name: Install npm Dependencies run: npm ci - name: Build WordPress + timeout-minutes: 5 run: npm run build - name: Prepare the directory structure for the ZIP @@ -58,3 +66,4 @@ jobs: name: wordpress-develop path: develop.zip if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/reusable-cleanup-pull-requests.yml b/.github/workflows/reusable-cleanup-pull-requests.yml index 012f99adbec3b..c0266864b924c 100644 --- a/.github/workflows/reusable-cleanup-pull-requests.yml +++ b/.github/workflows/reusable-cleanup-pull-requests.yml @@ -3,6 +3,8 @@ # tickets that are referenced as fixed in commit messages. # # More info about using GitHub pull requests for contributing to WordPress can be found in the handbook: https://make.wordpress.org/core/handbook/contribute/git/github-pull-requests-for-code-review/. +# +# This workflow is used by `trunk` and branches >= 6.8. ## name: Run pull request cleanup @@ -56,33 +58,45 @@ jobs: const githubSha = process.env.GITHUB_SHA; let prNumbers = []; + core.notice(`Processing pull request cleanup for SVN changeset: https://core.trac.wordpress.org/changeset/${svnRevisionNumber}`); + for (const ticket of fixedList) { const tracTicketUrl = `https://core.trac.wordpress.org/ticket/${ticket}`; const corePrefix = `core-${ticket}`; + core.info(`Ticket: ${tracTicketUrl}`); + const query = ` query($searchQuery: String!) { search(query: $searchQuery, type: ISSUE_ADVANCED, first: 20) { nodes { ... on PullRequest { number - bodyText + body } } } } `; - const searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:pr is:open ( "${tracTicketUrl}" OR "${corePrefix}" )`; + // The PR template only asks for "a link to the WordPress Trac ticket", so some + // PRs reference it as a Markdown link with just the ticket number as the link + // text (e.g. "Trac ticket: [65864](https://core.trac.wordpress.org/ticket/65864)"). + // GitHub's search index doesn't cover Markdown link targets, only the rendered + // text, so the bare ticket number is searched for too to surface those PRs. + const searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:pr is:open ( "${tracTicketUrl}" OR "${corePrefix}" OR "${ticket}" )`; const result = await github.graphql(query, { searchQuery, }); - // Since search queries will match anywhere for any activity on a pull request, the body specifically needs to be manually checked. + // Since search queries will match anywhere for any activity on a pull request, the body + // specifically needs to be manually checked. The raw `body` (rather than `bodyText`) is used + // here because `bodyText` strips Markdown links down to just their visible text, which would + // drop the Trac ticket URL entirely for PRs that link the ticket via its number. const matchingPRs = result.search.nodes .filter(pr => { - const bodyLower = pr.bodyText.toLowerCase(); + const bodyLower = pr.body.toLowerCase(); return bodyLower.includes(tracTicketUrl.toLowerCase()) || bodyLower.includes(corePrefix.toLowerCase()); }).map(pr => pr.number); @@ -97,8 +111,13 @@ jobs: This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.`; + const uniquePrNumbers = [...new Set(prNumbers)]; + core.info(`Found ${uniquePrNumbers.length} pull request(s) to close.`); + // Update all matched pull requests. - for (const prNumber of prNumbers) { + for (const prNumber of uniquePrNumbers) { + core.info(`Closing pull request: https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}`); + // Comment on the pull request with details. await github.rest.issues.createComment({ owner: context.repo.owner, diff --git a/.github/workflows/reusable-coding-standards-javascript.yml b/.github/workflows/reusable-coding-standards-javascript.yml index 8c0ea06345b85..c81ef7aa2c262 100644 --- a/.github/workflows/reusable-coding-standards-javascript.yml +++ b/.github/workflows/reusable-coding-standards-javascript.yml @@ -1,5 +1,7 @@ ## # A reusable workflow that checks the JavaScript coding standards. +# +# This workflow is used by `trunk` and branches >= 4.7. ## name: JavaScript coding standards @@ -43,7 +45,12 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: '.nvmrc' - cache: npm + package-manager-cache: false + + - name: Set up npm + # Release branches run this workflow from trunk, but some older branches may lack the action. + if: ${{ hashFiles('.github/actions/setup-npm/action.yml') != '' }} + uses: ./.github/actions/setup-npm - name: Log debug information run: | diff --git a/.github/workflows/reusable-coding-standards-php.yml b/.github/workflows/reusable-coding-standards-php.yml index f0ee3ae0479f2..52779b0aa29b0 100644 --- a/.github/workflows/reusable-coding-standards-php.yml +++ b/.github/workflows/reusable-coding-standards-php.yml @@ -1,5 +1,7 @@ ## # A reusable workflow that checks the PHP coding standards. +# +# This workflow is used by `trunk` and branches >= 5.1. ## name: PHP coding standards diff --git a/.github/workflows/reusable-end-to-end-tests.yml b/.github/workflows/reusable-end-to-end-tests.yml index 9b5586973aacf..54bd6b4230fab 100644 --- a/.github/workflows/reusable-end-to-end-tests.yml +++ b/.github/workflows/reusable-end-to-end-tests.yml @@ -1,8 +1,7 @@ ## # A reusable workflow that runs end-to-end tests. # -# Branches 6.3 and earlier used Puppeteer instead of Playwright. -# Use https://github.com/WordPress/wordpress-develop/tree/6.3/.github/workflows/reusable-end-to-end-tests.yml instead. +# This workflow is used by `trunk` and branches >= 5.8. ## name: End-to-end Tests @@ -85,7 +84,12 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: '.nvmrc' - cache: npm + package-manager-cache: false + + - name: Set up npm + # Release branches run this workflow from trunk, but some older branches may lack the action. + if: ${{ hashFiles('.github/actions/setup-npm/action.yml') != '' }} + uses: ./.github/actions/setup-npm - name: Log debug information run: | @@ -100,9 +104,10 @@ jobs: - name: Install Playwright browsers if: ${{ inputs.install-playwright }} - run: npm exec --no -- playwright install --with-deps chromium + run: npm exec --no -- playwright install chromium - name: Build WordPress + timeout-minutes: 5 run: npm run build - name: Start Docker environment @@ -143,6 +148,10 @@ jobs: - name: Run E2E tests run: npm run test:e2e + env: + # Chromium is installed above and is the only browser the suite runs. Without this, + # `wp-scripts test-playwright` also downloads Firefox and WebKit on every run. + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: ${{ inputs.install-playwright && '1' || '' }} - name: Archive debug artifacts (screenshots, HTML snapshots) uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -152,6 +161,7 @@ jobs: path: artifacts if-no-files-found: ignore include-hidden-files: true + retention-days: 14 - name: Ensure version-controlled files are not modified or deleted run: git diff --exit-code diff --git a/.github/workflows/reusable-javascript-tests.yml b/.github/workflows/reusable-javascript-tests.yml index c0bf656d124bb..62b237a515e66 100644 --- a/.github/workflows/reusable-javascript-tests.yml +++ b/.github/workflows/reusable-javascript-tests.yml @@ -1,5 +1,7 @@ ## # A reusable workflow that runs JavaScript tests. +# +# This workflow is used by `trunk` and branches >= 4.7. ## name: JavaScript tests @@ -44,7 +46,12 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: '.nvmrc' - cache: npm + package-manager-cache: false + + - name: Set up npm + # Release branches run this workflow from trunk, but some older branches may lack the action. + if: ${{ hashFiles('.github/actions/setup-npm/action.yml') != '' }} + uses: ./.github/actions/setup-npm - name: Log debug information run: | diff --git a/.github/workflows/reusable-javascript-type-checking-v1.yml b/.github/workflows/reusable-javascript-type-checking-v1.yml index 032fa67bf7be8..e0bf36ce19c9c 100644 --- a/.github/workflows/reusable-javascript-type-checking-v1.yml +++ b/.github/workflows/reusable-javascript-type-checking-v1.yml @@ -1,5 +1,7 @@ ## # A reusable workflow that runs JavaScript Type Checking. +# +# This workflow is used by `trunk` and branches >= 7.0. ## name: JavaScript Type Checking @@ -42,7 +44,12 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: '.nvmrc' - cache: npm + package-manager-cache: false + + - name: Set up npm + # Release branches run this workflow from trunk, but some older branches may lack the action. + if: ${{ hashFiles('.github/actions/setup-npm/action.yml') != '' }} + uses: ./.github/actions/setup-npm - name: Log debug information run: | diff --git a/.github/workflows/reusable-performance-report-v2.yml b/.github/workflows/reusable-performance-report-v2.yml index a2bce3e7c362b..56cc147491955 100644 --- a/.github/workflows/reusable-performance-report-v2.yml +++ b/.github/workflows/reusable-performance-report-v2.yml @@ -1,5 +1,7 @@ ## # A reusable workflow that compares and publishes the performance tests. +# +# This workflow is used by `trunk` and branches >= 6.8. ## name: Compare and publish performance Tests @@ -65,7 +67,7 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: '.nvmrc' - cache: npm + package-manager-cache: false - name: Download artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml index b92893f52f7f1..6b0e43e136c37 100644 --- a/.github/workflows/reusable-performance-test-v2.yml +++ b/.github/workflows/reusable-performance-test-v2.yml @@ -1,5 +1,7 @@ ## # A reusable workflow that runs the performance test suite. +# +# This workflow is used by `trunk` and branches >= 6.8. ## name: Run performance Tests @@ -126,7 +128,12 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: '.nvmrc' - cache: npm + package-manager-cache: false + + - name: Set up npm + # Release branches run this workflow from trunk, but some older branches may lack the action. + if: ${{ hashFiles('.github/actions/setup-npm/action.yml') != '' }} + uses: ./.github/actions/setup-npm - name: Set up PHP if: ${{ inputs.subject == 'base' }} @@ -154,6 +161,7 @@ jobs: run: npm run env:start - name: Build WordPress + timeout-minutes: 5 run: npm run build - name: Download previous build artifact (target branch or previous commit) @@ -271,6 +279,7 @@ jobs: path: artifacts if-no-files-found: error include-hidden-files: true + retention-days: 1 - name: Ensure version-controlled files are not modified or deleted run: git diff --exit-code diff --git a/.github/workflows/reusable-php-compatibility.yml b/.github/workflows/reusable-php-compatibility.yml index 156494118fced..d22ca18a66928 100644 --- a/.github/workflows/reusable-php-compatibility.yml +++ b/.github/workflows/reusable-php-compatibility.yml @@ -1,5 +1,7 @@ ## # A reusable workflow that runs PHP compatibility tests. +# +# This workflow is used by `trunk` and branches >= 5.5. ## name: PHP Compatibility diff --git a/.github/workflows/reusable-phpstan-static-analysis-v1.yml b/.github/workflows/reusable-phpstan-static-analysis-v1.yml index 26a14ba8d890f..321f98189cdf9 100644 --- a/.github/workflows/reusable-phpstan-static-analysis-v1.yml +++ b/.github/workflows/reusable-phpstan-static-analysis-v1.yml @@ -1,5 +1,7 @@ ## # A reusable workflow that runs PHP Static Analysis tests. +# +# This workflow is used by `trunk` and branches >= 7.0. ## name: PHP Static Analysis @@ -19,20 +21,21 @@ permissions: {} jobs: # Runs PHP static analysis tests. # - # Violations are reported inline with annotations. + # Violations are reported inline with annotations. For pull requests, the results are also uploaded + # as an artifact, which the PHPStan Pull Request Comments workflow summarizes in a comment. # # Performs the following steps: # - Checks out the repository. - # - Sets up Node.js. # - Sets up PHP. # - Logs debug information. # - Installs Composer dependencies. # - Make Composer packages available globally. - # - Installs npm dependencies. # - Builds WordPress. # - Configures caching for PHPStan static analysis scans. - # - Runs PHPStan static analysis (with Pull Request annotations). - # - Checks whether the baselines need regenerating. + # - Runs PHPStan static analysis. + # - Reports the results as annotations. + # - Uploads the results as an artifact (pull requests only). + # - Checks for errors that cannot be reported as warnings, such as out-of-date baselines. # - Saves the PHPStan result cache. # - Ensures version-controlled files are not modified or deleted. phpstan: @@ -43,29 +46,23 @@ jobs: timeout-minutes: 20 steps: + # For a pull request, the parents of the merge commit are fetched too, to find the files that the base branch + # has changed since the pull request branched from it. - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: '.nvmrc' - cache: npm + fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 1 }} - name: Set up PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: ${{ inputs.php-version }} coverage: none - tools: cs2pr - name: General debug information run: | - npm --version - node --version composer --version # Since Composer dependencies are installed using `composer update` and no lock file is in version control, @@ -78,52 +75,153 @@ jobs: - name: Make Composer packages available globally run: echo "${PWD}/vendor/bin" >> "$GITHUB_PATH" - - name: Install npm dependencies - run: npm ci --ignore-scripts - - - name: Build WordPress - run: npm run build:dev - + # The directory holds more than the analysis results. PHPStan also stores what it read out of + # each source file there, the docblocks and signatures it found, keyed by that file's contents + # and nothing else. The extensions in `tests/phpstan` change what reading a file yields without + # changing the file: a parser node visitor rewrites a docblock in the syntax tree, and the bytes + # on disk stay as they were. A cache written before one of them changed therefore answers with + # what the old code saw, and the analysis silently runs against types no longer derived. + # + # Keying on the configuration and the extensions keeps a run from restoring a cache that + # predates either. The baselines are left out of the key: they only decide which reported errors + # are ignored, PHPStan invalidates the results cache on a configuration change by itself, and + # including them would discard the whole cache every time one is regenerated. + # + # The cache is renamed rather than only prefixed, because the key this replaces restored on + # `phpstan-result-cache-`, which every key below still begins with. A branch that has not + # merged this commit yet runs the workflow it has, and that workflow would prefix-match the + # caches written here: it would analyse without the extensions against reflection derived with + # them. A name of its own leaves those runs restoring only their own caches. - name: Cache PHP Static Analysis scan cache uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: .cache # This is defined in the base.neon file. - key: "phpstan-result-cache-${{ github.run_id }}" + key: "phpstan-analysis-cache-${{ hashFiles('phpstan.neon.dist', 'tests/phpstan/*.neon', 'tests/phpstan/*.php') }}-${{ github.run_id }}" restore-keys: | - phpstan-result-cache- + phpstan-analysis-cache-${{ hashFiles('phpstan.neon.dist', 'tests/phpstan/*.neon', 'tests/phpstan/*.php') }}- - name: Run PHP static analysis tests id: phpstan run: | - # The report is written to a file as well as piped to cs2pr, so that the step below - # can look at it. + # The report is written to a file for the steps below, which turn it into annotations, + # the pull request comment, and a check for errors that cannot be reported as warnings. + # Only the report goes to standard output; the debug output goes to standard error, so + # it stays in the log without corrupting the JSON. # - # cs2pr exits successfully so that reported errors annotate the pull request without - # failing the run. A pipeline reports only the status of its last command, so that - # also discards the status of the analysis itself. Recover it from PIPESTATUS. - composer run phpstan -- -vvv --error-format=checkstyle | tee "${RUNNER_TEMP}/phpstan-report.xml" | cs2pr --errors-as-warnings --graceful-warnings - status="${PIPESTATUS[0]}" + # The step shell exits on the first failing command, which would also end the step on + # the status of 1 that PHPStan uses for reporting errors. Capture the status instead. + status=0 + composer run phpstan -- -vvv --error-format=json > "${RUNNER_TEMP}/phpstan-report.json" || status=$? # PHPStan exits 1 when it has errors to report, which is the expected case here and - # is what the annotations are for. Anything higher means it did not finish at all, - # which would otherwise pass silently, since the discarded status was the only sign. + # is what the annotations are for. Anything higher means it did not finish at all. if [ "${status}" -gt 1 ]; then echo "::error title=PHPStan did not complete::The analysis exited with status ${status}, so the code was not fully checked. This is a failure of the run itself rather than a problem found in the code." exit "${status}" fi - # An ignored error that no longer occurs, or occurs a different number of times, is - # reported under an `ignore.*` identifier. That is not something to fix in the code: the - # usual cause is that the error *was* fixed, leaving a baseline describing a state that - # no longer exists. PHPStan does not allow those reports to be ignored or baselined. + # Errors are annotated as warnings, so that they are visible on the pull request without + # failing the run. The exception is an error that PHPStan does not allow to be ignored, + # such as an out-of-date baseline entry, or one that is not specific to a file. These are + # annotated as errors instead, and the run fails on them in the check further below. # - # The analysis above is reported as warnings, so this would otherwise surface as a - # passing run carrying an annotation that reads like a complaint about a fix. Call it - # out on its own, and say what to do about it. + # The results are also written out for the PHPStan Pull Request Comments workflow, which + # runs with the permissions needed to comment on the pull request. Only the facts are + # passed along: the comment is composed there, since a pull request can change anything + # that runs here. + - name: Report the analysis results + id: report + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const fs = require( 'fs' ); + + const report = JSON.parse( fs.readFileSync( `${ process.env.RUNNER_TEMP }/phpstan-report.json`, 'utf8' ) ); + const workspace = `${ process.env.GITHUB_WORKSPACE }/`; + + const errors = []; + for ( const [ absolutePath, file ] of Object.entries( report.files ) ) { + const relativePath = absolutePath.startsWith( workspace ) ? absolutePath.slice( workspace.length ) : absolutePath; + for ( const message of file.messages ) { + errors.push( { + file: relativePath, + line: message.line ?? null, + message: message.message, + identifier: message.identifier ?? null, + tip: message.tip ?? null, + ignorable: message.ignorable, + } ); + } + } + + // Errors that are not specific to a file, such as an `ignoreErrors` entry without a + // path that no longer matches. These cannot be ignored either. + for ( const message of report.errors ) { + errors.push( { file: null, line: null, message, identifier: null, tip: null, ignorable: false } ); + } + + for ( const error of errors ) { + const properties = { title: error.identifier ? `PHPStan: ${ error.identifier }` : 'PHPStan' }; + if ( error.file ) { + properties.file = error.file; + } + if ( error.line ) { + properties.startLine = error.line; + } + const message = error.tip ? `${ error.message }\n${ error.tip }` : error.message; + if ( error.ignorable ) { + core.warning( message, properties ); + } else { + core.error( message, properties ); + } + } + + // Tips are left out of the results, since they are only shown in the annotations, next to the code. + // + // The commit analyzed is the pull request merged into its base branch, whose second parent is the head + // commit. The files with errors that differ between the two are those the base branch has changed, in + // which the line numbers reported can be off in the head commit. + if ( context.eventName === 'pull_request' ) { + let changedInBase = []; + try { + const filesWithErrors = new Set( errors.map( ( error ) => error.file ) ); + changedInBase = require( 'child_process' ) + .execFileSync( 'git', [ 'diff', '--name-only', '--no-renames', 'HEAD^2', 'HEAD' ], { encoding: 'utf8' } ) + .split( '\n' ) + .filter( ( file ) => filesWithErrors.has( file ) ); + } catch ( error ) { + core.warning( `The files changed in the base branch could not be found: ${ error.message }` ); + } + fs.mkdirSync( `${ process.env.RUNNER_TEMP }/phpstan-results`, { recursive: true } ); + fs.writeFileSync( `${ process.env.RUNNER_TEMP }/phpstan-results/results.json`, JSON.stringify( { + pull_request: context.payload.pull_request.number, + changed_in_base: changedInBase, + errors: errors.map( ( { tip, ...error } ) => error ), + } ) ); + } + + # Uploads the results for the PHPStan Pull Request Comments workflow, which keeps a comment on the + # pull request up to date with them. + - name: Upload the analysis results as an artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ github.event_name == 'pull_request' && steps.report.outcome == 'success' }} + with: + name: phpstan-results + path: ${{ runner.temp }}/phpstan-results/ + + # Some errors cannot be ignored, so they cannot be reported as warnings either. Most often + # this is an ignored error that no longer occurs, or occurs a different number of times, + # which is reported under an `ignore.*` identifier. That is not something to fix in the + # code: the usual cause is that the error *was* fixed, leaving a baseline describing a state + # that no longer exists. # - # Detection is on the identifier rather than the message, which is prose and may be - # reworded in any release. The checkstyle format carries it in the `source` attribute. - - name: Check whether the baselines need regenerating + # Such an error is reported without a line number, so its annotation is not shown alongside + # the changes in the pull request, and one that is not specific to a file has nowhere to be + # shown at all. A passing run would let it go unnoticed, so fail the run on it instead, and + # say what to do about it. + # + # Detection is on PHPStan's own `ignorable` flag rather than on the identifier or message. + - name: Check for errors that cannot be reported as warnings if: ${{ !cancelled() }} env: BASELINES_URL: ${{ github.server_url }}/${{ github.repository }}/tree/${{ github.sha }}/tests/phpstan/baselines @@ -132,20 +230,28 @@ jobs: # This step runs even when the analysis above it failed, in which case the report may # never have been written. That failure is reported there, so there is nothing to add # here beyond staying quiet about a file that was never going to exist. - if [ ! -f "${RUNNER_TEMP}/phpstan-report.xml" ]; then + report="${RUNNER_TEMP}/phpstan-report.json" + if [ ! -f "${report}" ]; then exit 0 fi - # Leave the run alone unless PHPStan reported an ignore error, because everything - # below concerns an ignore configuration that no longer describes the code, and - # nothing else. Those errors are `ignore.unmatched`, where a pattern matched nothing - # at all, and `ignore.count`, where it matched a different number of times than the - # entry records. The `ignore.` prefix is matched rather than those two names so that - # any later addition to the group is caught as well. - if ! grep -q 'source="ignore\.' "${RUNNER_TEMP}/phpstan-report.xml"; then + # Errors that are not specific to a file are listed separately from the rest, and are + # never ignorable. + unignorable=$( jq '[ .files[].messages[] | select( .ignorable | not ) ] + .errors | length' "${report}" ) + if [ "${unignorable}" -eq 0 ]; then exit 0 fi + # Whether the errors include ignore errors decides what to say about them, but not whether to + # fail: every error that reaches this point fails the run. Ignore errors concern an ignore + # configuration that no longer describes the code. They are `ignore.unmatched`, where a + # pattern matched nothing at all, and `ignore.count`, where it matched a different number of + # times than the entry records. The `ignore.` prefix is matched rather than those two names + # so that any later addition to the group is caught as well. Any other error that cannot be + # ignored is something regenerating the baselines would not help with, so it is reported + # plainly instead. + ignore_errors=$( jq '[ .files[].messages[] | select( .identifier // "" | startswith( "ignore." ) ) ] | length' "${report}" ) + # The summary is Markdown, and its code spans and fences are written literally, so the # heredoc is quoted to keep the backticks out of the shell's hands. The links are # written in reference style for the same reason: the URLs are the only part needing @@ -153,7 +259,7 @@ jobs: # # A newline renders as a line break rather than a space, so each paragraph is one # line however long that makes it, and the rendered summary wraps to its own width. - cat >> "${GITHUB_STEP_SUMMARY}" <<'SUMMARY' + baselines_summary=$( cat <<'SUMMARY' ## PHPStan baselines are out of date An ignored error no longer occurs, or occurs a different number of times, so PHPStan reported it under an `ignore.unmatched` or `ignore.count` identifier. @@ -179,13 +285,20 @@ jobs: See [`tests/phpstan/README.md`][readme] for details. SUMMARY + ) - { - echo "[baselines]: ${BASELINES_URL}" - echo "[readme]: ${README_URL}" - } >> "${GITHUB_STEP_SUMMARY}" + if [ "${ignore_errors}" -gt 0 ]; then + { + printf '%s\n\n' "${baselines_summary}" + echo "[baselines]: ${BASELINES_URL}" + echo "[readme]: ${README_URL}" + } >> "${GITHUB_STEP_SUMMARY}" + + echo "::error title=PHPStan baselines are out of date::An ignored error no longer occurs, or occurs a different number of times. If you fixed it, that is expected: run \`npm run typecheck:php:baselines\` or \`composer phpstan:baselines\` and commit the updated baselines. See ${README_URL}" + else + echo "::error title=PHPStan reported errors that cannot be ignored::PHPStan reported ${unignorable} error(s) that cannot be ignored or reported as warnings. See the error annotations for details, and ${README_URL} for how to run the analysis locally." + fi - echo "::error title=PHPStan baselines are out of date::An ignored error no longer occurs, or occurs a different number of times. If you fixed it, that is expected: run \`npm run typecheck:php:baselines\` or \`composer phpstan:baselines\` and commit the updated baselines. See ${README_URL}" exit 1 - name: "Save result cache" @@ -193,7 +306,7 @@ jobs: if: ${{ !cancelled() }} with: path: .cache - key: "phpstan-result-cache-${{ github.run_id }}" + key: "phpstan-analysis-cache-${{ hashFiles('phpstan.neon.dist', 'tests/phpstan/*.neon', 'tests/phpstan/*.php') }}-${{ github.run_id }}" - name: Ensure version-controlled files are not modified or deleted run: git diff --exit-code diff --git a/.github/workflows/reusable-phpunit-tests-v1.yml b/.github/workflows/reusable-phpunit-tests-v1.yml index 13c87c62671f8..dc5a0d1edd982 100644 --- a/.github/workflows/reusable-phpunit-tests-v1.yml +++ b/.github/workflows/reusable-phpunit-tests-v1.yml @@ -15,6 +15,16 @@ on: required: false type: 'string' default: 'ubuntu-24.04' + repository: + description: 'The repository to check out and test. Defaults to the repository calling this workflow.' + required: false + type: 'string' + default: '' + ref: + description: 'The branch, tag, or SHA to check out. Defaults to the ref that triggered the calling workflow.' + required: false + type: 'string' + default: '' php: description: 'The version of PHP to use, in the format of X.Y' required: true @@ -44,11 +54,21 @@ on: required: false type: 'string' default: 'phpunit.xml.dist' + phpunit-test-groups: + description: 'A list of test groups to run.' + required: false + type: 'string' + default: '' allow-errors: description: 'Whether to continue when test errors occur.' required: false type: boolean default: false + overlay-artifact: + description: 'The name of a same-workflow artifact whose contents are unpacked over the checkout. Optional: for callers whose test files are not part of the repository being tested.' + required: false + type: string + default: '' env: COMPOSER_INSTALL: false LOCAL_PHP: ${{ inputs.php }}-fpm @@ -91,6 +111,25 @@ jobs: contents: read steps: + # `ref` otherwise falls back to checkout's own default, which for another repository is + # its default branch: a "WordPress 6.2" job would test trunk and report green. + - name: Require a ref when testing another repository + if: ${{ inputs.repository != '' && inputs.ref == '' }} + run: | + echo 'Testing another repository needs an explicit ref.' >&2 + exit 1 + + # Legacy docker.js reparses arguments as shell: allow only group names. + - name: Validate test groups + if: ${{ inputs.phpunit-test-groups != '' }} + env: + TEST_GROUPS: ${{ inputs.phpunit-test-groups }} + run: | + if [[ ! "$TEST_GROUPS" =~ ^[A-Za-z0-9_.-]+(,[A-Za-z0-9_.-]+)*$ ]]; then + echo 'phpunit-test-groups must be a comma-separated list of group names.' >&2 + exit 1 + fi + - name: Configure environment variables run: | echo "PHP_FPM_UID=$(id -u)" >> "$GITHUB_ENV" @@ -99,9 +138,21 @@ jobs: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref }} show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} persist-credentials: false + # If the caller specifies an overlay artifact then unpack it over the checkout. + - name: Download overlay artifact + if: inputs.overlay-artifact != '' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + # Without a run ID, this action reads only from the caller's current workflow run. + name: ${{ inputs.overlay-artifact }} + path: . + digest-mismatch: error + - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: @@ -173,25 +224,27 @@ jobs: run: npm run env:install - name: Run slow PHPUnit tests - if: ${{ inputs.split_slow }} + if: ${{ inputs.split_slow && ! inputs.phpunit-test-groups }} run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --group "${SLOW_TESTS}" - name: Run PHPUnit tests for single site excluding slow tests - if: ${{ inputs.php < '7.0' && ! inputs.split_slow && ! inputs.multisite }} + if: ${{ inputs.php < '7.0' && ! inputs.split_slow && ! inputs.multisite && ! inputs.phpunit-test-groups }} run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --exclude-group "${SLOW_TESTS},ajax,ms-files,ms-required" - name: Run PHPUnit tests for Multisite excluding slow tests - if: ${{ inputs.php < '7.0' && ! inputs.split_slow && inputs.multisite }} + if: ${{ inputs.php < '7.0' && ! inputs.split_slow && inputs.multisite && ! inputs.phpunit-test-groups }} run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --exclude-group "${SLOW_TESTS},ajax,ms-files,ms-excluded,oembed-headers" - name: Run PHPUnit tests - if: ${{ inputs.php >= '7.0' }} - run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" + if: ${{ inputs.php >= '7.0' || inputs.phpunit-test-groups }} + run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" ${{ inputs.phpunit-test-groups && '--group "${TEST_GROUPS}"' || '' }} + env: + TEST_GROUPS: ${{ inputs.phpunit-test-groups }} - name: Run AJAX tests - if: ${{ ! inputs.multisite && ! inputs.split_slow }} + if: ${{ ! inputs.multisite && ! inputs.split_slow && ! inputs.phpunit-test-groups }} run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --group ajax - name: Run external HTTP tests - if: ${{ ! inputs.multisite && ! inputs.split_slow }} + if: ${{ ! inputs.multisite && ! inputs.split_slow && ! inputs.phpunit-test-groups }} run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c phpunit.xml.dist --group external-http diff --git a/.github/workflows/reusable-phpunit-tests-v2.yml b/.github/workflows/reusable-phpunit-tests-v2.yml index 15dea5fc31efa..628b5387f0e4f 100644 --- a/.github/workflows/reusable-phpunit-tests-v2.yml +++ b/.github/workflows/reusable-phpunit-tests-v2.yml @@ -15,6 +15,16 @@ on: required: false type: 'string' default: 'ubuntu-24.04' + repository: + description: 'The repository to check out and test. Defaults to the repository calling this workflow.' + required: false + type: 'string' + default: '' + ref: + description: 'The branch, tag, or SHA to check out. Defaults to the ref that triggered the calling workflow.' + required: false + type: 'string' + default: '' php: description: 'The version of PHP to use, in the format of X.Y' required: true @@ -44,6 +54,11 @@ on: required: false type: 'string' default: 'phpunit.xml.dist' + phpunit-test-groups: + description: 'A list of test groups to run.' + required: false + type: 'string' + default: '' report: description: 'Whether to report results to WordPress.org Hosting Tests' required: false @@ -54,6 +69,11 @@ on: required: false type: boolean default: false + overlay-artifact: + description: 'The name of a same-workflow artifact whose contents are unpacked over the checkout. Optional: for callers whose test files are not part of the repository being tested.' + required: false + type: string + default: '' env: LOCAL_PHP: ${{ inputs.php }}-fpm LOCAL_PHP_MEMCACHED: ${{ inputs.memcached }} @@ -93,6 +113,25 @@ jobs: contents: read steps: + # `ref` otherwise falls back to checkout's own default, which for another repository is + # its default branch: a "WordPress 6.2" job would test trunk and report green. + - name: Require a ref when testing another repository + if: ${{ inputs.repository != '' && inputs.ref == '' }} + run: | + echo 'Testing another repository needs an explicit ref.' >&2 + exit 1 + + # Legacy docker.js reparses arguments as shell: allow only group names. + - name: Validate test groups + if: ${{ inputs.phpunit-test-groups != '' }} + env: + TEST_GROUPS: ${{ inputs.phpunit-test-groups }} + run: | + if [[ ! "$TEST_GROUPS" =~ ^[A-Za-z0-9_.-]+(,[A-Za-z0-9_.-]+)*$ ]]; then + echo 'phpunit-test-groups must be a comma-separated list of group names.' >&2 + exit 1 + fi + - name: Configure environment variables run: | echo "PHP_FPM_UID=$(id -u)" >> "$GITHUB_ENV" @@ -101,9 +140,21 @@ jobs: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref }} show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} persist-credentials: false + # If the caller specifies an overlay artifact then unpack it over the checkout. + - name: Download overlay artifact + if: inputs.overlay-artifact != '' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + # Without a run ID, this action reads only from the caller's current workflow run. + name: ${{ inputs.overlay-artifact }} + path: . + digest-mismatch: error + - name: Install Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: @@ -176,36 +227,38 @@ jobs: run: npm run env:install - name: Run slow PHPUnit tests - if: ${{ inputs.split_slow }} + if: ${{ inputs.split_slow && ! inputs.phpunit-test-groups }} run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --group "${SLOW_TESTS}" - name: Run PHPUnit tests for single site excluding slow tests - if: ${{ inputs.php < '7.0' && ! inputs.split_slow && ! inputs.multisite }} + if: ${{ inputs.php < '7.0' && ! inputs.split_slow && ! inputs.multisite && ! inputs.phpunit-test-groups }} run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --exclude-group "${SLOW_TESTS},ajax,ms-files,ms-required" - name: Run PHPUnit tests for Multisite excluding slow tests - if: ${{ inputs.php < '7.0' && ! inputs.split_slow && inputs.multisite }} + if: ${{ inputs.php < '7.0' && ! inputs.split_slow && inputs.multisite && ! inputs.phpunit-test-groups }} run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --exclude-group "${SLOW_TESTS},ajax,ms-files,ms-excluded,oembed-headers" - name: Run PHPUnit tests - if: ${{ inputs.php >= '7.0' }} - run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" + if: ${{ inputs.php >= '7.0' || inputs.phpunit-test-groups }} + run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" ${{ inputs.phpunit-test-groups && '--group "${TEST_GROUPS}"' || '' }} + env: + TEST_GROUPS: ${{ inputs.phpunit-test-groups }} - name: Run AJAX tests - if: ${{ ! inputs.split_slow&& inputs.test_ajax }} + if: ${{ ! inputs.split_slow&& inputs.test_ajax && ! inputs.phpunit-test-groups }} run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --group ajax - name: Run ms-files tests as a multisite install - if: ${{ inputs.multisite && ! inputs.split_slow }} + if: ${{ inputs.multisite && ! inputs.split_slow && ! inputs.phpunit-test-groups }} run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --group ms-files - name: Run external HTTP tests - if: ${{ ! inputs.multisite && ! inputs.split_slow }} + if: ${{ ! inputs.multisite && ! inputs.split_slow && ! inputs.phpunit-test-groups }} run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c phpunit.xml.dist --group external-http # __fakegroup__ is excluded to force PHPUnit to ignore the settings in phpunit.xml.dist. - name: Run (xDebug) tests - if: ${{ ! inputs.split_slow }} + if: ${{ ! inputs.split_slow && ! inputs.phpunit-test-groups }} run: LOCAL_PHP_XDEBUG=true npm run "test:${PHPUNIT_SCRIPT}" -- -v --group xdebug --exclude-group __fakegroup__ - name: Ensure version-controlled files are not modified or deleted diff --git a/.github/workflows/reusable-phpunit-tests-v3.yml b/.github/workflows/reusable-phpunit-tests-v3.yml index 4f7addcd4e60e..1e1c85ad0a453 100644 --- a/.github/workflows/reusable-phpunit-tests-v3.yml +++ b/.github/workflows/reusable-phpunit-tests-v3.yml @@ -13,6 +13,18 @@ on: required: false type: 'string' default: 'ubuntu-24.04' + repository: + description: 'The repository to check out and test. Defaults to the repository calling this workflow.' + required: false + type: 'string' + default: '' + # Also composes the Gutenberg build name, under the producer's constraint: a valid + # artifact name, so no slashes and none of : < > | * ? " + ref: + description: 'The branch, tag, or SHA to check out. Defaults to the ref that triggered the calling workflow.' + required: false + type: 'string' + default: '' php: description: 'The version of PHP to use, in the format of X.Y' required: true @@ -82,7 +94,15 @@ on: required: false type: string default: '' + overlay-artifact: + description: 'The name of a same-workflow artifact whose contents are unpacked over the checkout. Optional: for callers whose test files are not part of the repository being tested.' + required: false + type: string + default: '' secrets: + CODEVITALS_PROJECT_TOKEN: + description: 'The authorization token for publishing results to CodeVitals.' + required: false CODECOV_TOKEN: description: 'The Codecov token required for uploading reports.' required: false @@ -109,8 +129,10 @@ jobs: # Runs the PHPUnit tests for WordPress. # # Performs the following steps: + # - Requires a ref when the caller points at another repository. # - Sets environment variables. # - Checks out the repository. + # - Unpacks the caller-provided overlay artifact over the checkout, if any. # - Downloads the prepared Gutenberg build provided by the calling workflow. # - Sets up Node.js. # - Sets up PHP. @@ -123,6 +145,8 @@ jobs: # - Logs debug information about what's installed within the WordPress Docker containers. # - Install WordPress within the Docker container. # - Run the PHPUnit tests. + # - Flags slow PHPUnit tests with GitHub Actions annotations and a run summary. + # - Publish PHPUnit timing metrics to CodeVitals. # - Upload the code coverage report to Codecov.io. # - Ensures version-controlled files are not modified or deleted. # - Checks out the WordPress Test reporter repository. @@ -130,11 +154,32 @@ jobs: phpunit-tests: name: ${{ ( inputs.phpunit-test-groups || inputs.coverage-report ) && format( 'PHP {0} with ', inputs.php ) || '' }} ${{ 'mariadb' == inputs.db-type && 'MariaDB' || 'MySQL' }} ${{ inputs.db-version }}${{ inputs.multisite && ' multisite' || '' }}${{ inputs.db-innovation && ' (innovation release)' || '' }}${{ inputs.memcached && ' with memcached' || '' }}${{ inputs.report && ' (test reporting enabled)' || '' }} ${{ 'example.org' != inputs.tests-domain && inputs.tests-domain || '' }} runs-on: ${{ vars.RUNNERS_NAME || inputs.os }} - timeout-minutes: ${{ inputs.coverage-report && 120 || inputs.php == '8.4' && 30 || 20 }} + timeout-minutes: ${{ inputs.coverage-report && 120 || 30 }} permissions: contents: read steps: + # `ref` otherwise falls back to checkout's own default, which for another repository is + # its default branch: a "WordPress 6.2" job would test trunk and report green. + - name: Require a ref when testing another repository + if: ${{ inputs.repository != '' && inputs.ref == '' }} + run: | + echo 'Testing another repository needs an explicit ref.' >&2 + exit 1 + + # `ref` also composes the Gutenberg artifact name, so reject what that name cannot carry + # here rather than at a download step the job only reaches minutes later. + - name: Require a ref the artifact name accepts + if: ${{ inputs.ref != '' }} + env: + REF: ${{ inputs.ref }} + run: | + invalid='[/:<>|*?"\]' + if [[ "$REF" =~ $invalid ]]; then + echo "A ref that names an artifact cannot contain / : < > | * ? \" \\ - received: $REF" >&2 + exit 1 + fi + - name: Configure environment variables run: | echo "PHP_FPM_UID=$(id -u)" >> "$GITHUB_ENV" @@ -143,15 +188,30 @@ jobs: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref }} show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} persist-credentials: false + # If the caller specifies an overlay artifact then unpack it over the checkout. + - name: Download overlay artifact + if: inputs.overlay-artifact != '' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + # Without a run ID, this action reads only from the caller's current workflow run. + name: ${{ inputs.overlay-artifact }} + path: . + digest-mismatch: error + # Only WordPress 7.0+ include Gutenberg-maintained assets from a built zip file. - name: Download prepared Gutenberg build if: ${{ inputs.gutenberg-artifact }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: gutenberg-build + # Every branch in a run shares one artifact namespace, so the producer names each + # build after the ref it came from. Compose the same name here rather than taking + # it as an input: the two sides already agree on `ref`. + name: gutenberg-build${{ inputs.ref && format( '-{0}', inputs.ref ) || '' }} path: gutenberg digest-mismatch: error @@ -159,7 +219,12 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: '.nvmrc' - cache: npm + package-manager-cache: false + + - name: Set up npm + # Release branches run this workflow from trunk, but some older branches may lack the action. + if: ${{ hashFiles('.github/actions/setup-npm/action.yml') != '' }} + uses: ./.github/actions/setup-npm ## # This allows Composer dependencies to be installed using a single step. @@ -185,6 +250,7 @@ jobs: run: npm ci - name: Build WordPress + timeout-minutes: 5 run: npm run build:dev env: # The producer resolved this once. Matrix jobs must not re-resolve mutable GHCR tags. @@ -256,6 +322,7 @@ jobs: continue-on-error: ${{ inputs.allow-errors }} run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c "${PHPUNIT_CONFIG}" --group external-http + # PHPUnit versions used below PHP 7.3 do not support --fail-on-empty-test-suite. - name: Run PHPUnit tests${{ inputs.phpunit-test-groups && format( ' ({0} groups)', inputs.phpunit-test-groups ) || '' }}${{ inputs.coverage-report && ' with coverage report' || '' }} continue-on-error: ${{ inputs.allow-errors }} run: | @@ -264,11 +331,69 @@ jobs: --verbose \ -c "${PHPUNIT_CONFIG}" \ ${{ inputs.phpunit-test-groups && '--group "${TEST_GROUPS}"' || '' }} \ + ${{ inputs.phpunit-test-groups && inputs.php >= '7.3' && '--fail-on-empty-test-suite' || '' }} \ ${{ inputs.coverage-report && '--coverage-clover "wp-code-coverage-${MULTISITE_FLAG}-${GITHUB_SHA}.xml"' || '' }} env: TEST_GROUPS: ${{ inputs.phpunit-test-groups }} MULTISITE_FLAG: ${{ inputs.multisite && 'multisite' || 'single' }} + - name: Flag slow PHPUnit tests + # Runs when the test step succeeds or fails, but skips cancelled jobs. + # This step never fails the job itself (continue-on-error), because the + # slow-test signal is advisory only. + if: >- + ${{ + ! cancelled() && inputs.report && inputs.php == '8.5' && + ( github.event_name == 'pull_request' || + ( github.event_name == 'push' && github.ref == 'refs/heads/trunk' ) ) + }} + continue-on-error: true + run: | + if [ -f tests/phpunit/build/logs/junit.xml ]; then + php tests/phpunit/prepare-slow-test-annotations.php \ + tests/phpunit/build/logs/junit.xml \ + 1.0 \ + 20 \ + "$RUNNER_TEMP/phpunit-timing-metrics.json" + else + echo 'PHPUnit JUnit report not found; skipping slow-test annotations.' + fi + + - name: Publish PHPUnit timing metrics + continue-on-error: true + if: | + github.event_name == 'push' && + github.ref == 'refs/heads/trunk' && + inputs.php == '8.5' && + inputs.report + env: + CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} + shell: bash + run: | + if [ -z "$CODEVITALS_PROJECT_TOKEN" ]; then + echo "PHPUnit timing metrics could not be published. 'CODEVITALS_PROJECT_TOKEN' is not set" + exit 1 + fi + COMMITTED_AT="$(git show -s "$GITHUB_SHA" --format='%cI')" + RESPONSE="$( + php tests/phpunit/prepare-timing-results.php \ + tests/phpunit/build/logs/junit.xml \ + trunk \ + "$GITHUB_SHA" \ + "$COMMITTED_AT" \ + "$RUNNER_TEMP/phpunit-timing-metrics.json" \ + | curl --fail-with-body --silent --show-error \ + --request POST \ + --header 'Content-Type: application/json' \ + --data-binary @- \ + "https://codevitals.run/api/log?token=${CODEVITALS_PROJECT_TOKEN}" + )" + if ! php -r "\$d = json_decode(file_get_contents('php://stdin'), true); exit(is_array(\$d) && (\$d['status'] ?? null) === 'ok' && (int) (\$d['count'] ?? 0) === 6 ? 0 : 1);" <<< "$RESPONSE"; then + echo 'CodeVitals did not accept all six PHPUnit timing metrics.' + exit 1 + fi + echo 'Published six PHPUnit timing metrics to CodeVitals.' + - name: Run AJAX tests if: ${{ ! inputs.phpunit-test-groups && ! inputs.coverage-report }} continue-on-error: ${{ inputs.allow-errors }} @@ -310,4 +435,4 @@ jobs: if: ${{ github.ref == 'refs/heads/trunk' && inputs.report }} env: WPT_REPORT_API_KEY: "${{ secrets.WPT_REPORT_API_KEY }}" - run: docker compose run --rm -e WPT_REPORT_API_KEY -e WPT_PREPARE_DIR=/var/www -e WPT_TEST_DIR=/var/www php php test-runner/report.php + run: docker compose run --rm -e WPT_REPORT_API_KEY -e WPT_PREPARE_DIR=/var/www -e WPT_TEST_DIR=/var/www -e WPT_DB_HOST=mysql -e WPT_DB_USER=root -e WPT_DB_PASSWORD=password -e WPT_DB_NAME=wordpress_develop_tests php php test-runner/report.php diff --git a/.github/workflows/reusable-prepare-gutenberg.yml b/.github/workflows/reusable-prepare-gutenberg.yml index c5e1f904a9680..fd7381550f349 100644 --- a/.github/workflows/reusable-prepare-gutenberg.yml +++ b/.github/workflows/reusable-prepare-gutenberg.yml @@ -6,6 +6,19 @@ name: Prepare Gutenberg build on: workflow_call: + inputs: + repository: + description: 'The repository to check out. Defaults to the repository calling this workflow.' + required: false + type: 'string' + default: '' + # Names the uploaded build too, so it must also be a valid artifact name: no slashes, + # and none of : < > | * ? " — a `refs/heads/trunk` or `feature/x` value fails the upload. + ref: + description: 'The branch, tag, or SHA to check out. Defaults to the commit that started the calling workflow run.' + required: false + type: 'string' + default: '' outputs: gutenberg-sha: description: 'The immutable Gutenberg source SHA verified by this workflow.' @@ -19,18 +32,41 @@ jobs: prepare-gutenberg: name: Gutenberg runs-on: ubuntu-24.04 - timeout-minutes: 10 + # Two download attempts of up to six minutes each, where download.js retries internally. + timeout-minutes: 15 outputs: gutenberg-sha: ${{ steps.download.outputs.gutenberg-sha }} permissions: contents: read steps: + # `ref` otherwise falls back to this run's commit, which another repository does not have. + - name: Require a ref when preparing another repository + if: ${{ inputs.repository != '' && inputs.ref == '' }} + run: | + echo 'Preparing another repository needs an explicit ref.' >&2 + exit 1 + + # The upload names the artifact after `ref` and is this job's last step, so an unusable + # value would otherwise fail only once the whole download and verify has already run. + - name: Require a ref the artifact name accepts + if: ${{ inputs.ref != '' }} + env: + REF: ${{ inputs.ref }} + run: | + invalid='[/:<>|*?"\]' + if [[ "$REF" =~ $invalid ]]; then + echo "A ref that names an artifact cannot contain / : < > | * ? \" \\ - received: $REF" >&2 + exit 1 + fi + - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - # Resolve the Gutenberg ref from the exact commit that started this workflow run. - ref: ${{ github.sha }} + repository: ${{ inputs.repository || github.repository }} + # Resolve the Gutenberg ref from the exact commit that started this workflow run, + # unless the caller is preparing a build for a repository or branch of its own. + ref: ${{ inputs.ref || github.sha }} show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} persist-credentials: false @@ -38,11 +74,22 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: '.nvmrc' + package-manager-cache: false - name: Download and verify Gutenberg build id: download run: | - node tools/gutenberg/download.js + # Branches whose download.js predates the in-script retry stream the blob straight + # into tar in a single attempt, so an interrupted stream fails the run outright. + # One retry, not two: where download.js does retry, it already allows three tries of + # two minutes each, and a third attempt here would outrun `timeout-minutes` and have + # the job killed rather than reported. + if ! node tools/gutenberg/download.js; then + echo 'Gutenberg download failed; retrying in 5 seconds...' + sleep 5 + node tools/gutenberg/download.js + fi + gutenberg_sha="$(tr -d '\n' < gutenberg/.gutenberg-hash)" if [[ ! "$gutenberg_sha" =~ ^[a-fA-F0-9]{40}$ ]]; then echo "Expected a 40-character Gutenberg SHA, received: $gutenberg_sha" >&2 @@ -53,8 +100,11 @@ jobs: - name: Upload Gutenberg build uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: gutenberg-build + # Every branch in a run shares one artifact namespace, so name the build after the + # ref it came from. The PHPUnit consumer composes the same name from its own `ref`, + # so the name is never passed between them. + name: gutenberg-build${{ inputs.ref && format( '-{0}', inputs.ref ) || '' }} path: gutenberg/ if-no-files-found: error include-hidden-files: true - retention-days: 1 + retention-days: 7 diff --git a/.github/workflows/reusable-support-json-reader-v1.yml b/.github/workflows/reusable-support-json-reader-v1.yml index d193b4a9ea92e..a44aa103208e5 100644 --- a/.github/workflows/reusable-support-json-reader-v1.yml +++ b/.github/workflows/reusable-support-json-reader-v1.yml @@ -1,6 +1,8 @@ ## # A reusable workflow that reads the .version-support-*.json files and returns values for use in a # test matrix based on a given WordPress version. +# +# This workflow is used by `trunk` and branches >= 6.8. ## name: Determine test matrix values diff --git a/.github/workflows/reusable-test-core-build-process.yml b/.github/workflows/reusable-test-core-build-process.yml index 1a41257418dec..8b42e2b75b72e 100644 --- a/.github/workflows/reusable-test-core-build-process.yml +++ b/.github/workflows/reusable-test-core-build-process.yml @@ -1,5 +1,7 @@ ## # A reusable workflow that tests the WordPress Core build process. +# +# This workflow is used by `trunk` and branches >= 4.7. ## name: Test the WordPress Build Process @@ -94,7 +96,12 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: '.nvmrc' - cache: npm + package-manager-cache: false + + - name: Set up npm + # Release branches run this workflow from trunk, but some older branches may lack the action. + if: ${{ hashFiles('.github/actions/setup-npm/action.yml') != '' }} + uses: ./.github/actions/setup-npm - name: Log debug information run: | @@ -121,6 +128,7 @@ jobs: run: npm run grunt copy:certificates && npm run grunt build:certificates - name: Build WordPress to run from ${{ inputs.directory }} + timeout-minutes: 5 run: npm run ${{ inputs.directory == 'src' && 'build:dev' || 'build' }} - name: Ensure version-controlled files are not modified or deleted during building @@ -143,6 +151,7 @@ jobs: name: wordpress-build-${{ github.event_name == 'pull_request' && github.event.number || github.sha }} path: wordpress.zip if-no-files-found: error + retention-days: 14 - name: Save PR number if: ${{ inputs.prepare-playground }} diff --git a/.github/workflows/reusable-test-gutenberg-build-process.yml b/.github/workflows/reusable-test-gutenberg-build-process.yml index a538daeed6779..e9c68d232807f 100644 --- a/.github/workflows/reusable-test-gutenberg-build-process.yml +++ b/.github/workflows/reusable-test-gutenberg-build-process.yml @@ -1,5 +1,9 @@ ## +# DEPRECATED +# # A reusable workflow that tests the Gutenberg plugin build process when run within a wordpress-develop checkout. +# +# This workflow is used by branches 6.5 through 6.9. ## name: Test the Gutenberg plugin Build Process @@ -16,6 +20,11 @@ on: required: false type: 'string' default: 'src' + gutenberg-branch: + description: 'The branch in the Gutenberg repository that should be tested against.' + required: false + type: 'string' + default: 'trunk' env: GUTENBERG_DIRECTORY: ${{ inputs.directory == 'build' && 'build' || 'src' }}/wp-content/plugins/gutenberg @@ -58,6 +67,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: 'WordPress/gutenberg' + ref: ${{ inputs.gutenberg-branch }} path: ${{ env.GUTENBERG_DIRECTORY }} show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} persist-credentials: false diff --git a/.github/workflows/reusable-test-local-docker-environment-v1.yml b/.github/workflows/reusable-test-local-docker-environment-v1.yml index 71c8a9a739f66..5d63fbc9aa4b4 100644 --- a/.github/workflows/reusable-test-local-docker-environment-v1.yml +++ b/.github/workflows/reusable-test-local-docker-environment-v1.yml @@ -95,7 +95,12 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: '.nvmrc' - cache: npm + package-manager-cache: false + + - name: Set up npm + # Release branches run this workflow from trunk, but some older branches may lack the action. + if: ${{ hashFiles('.github/actions/setup-npm/action.yml') != '' }} + uses: ./.github/actions/setup-npm ## # This allows Composer dependencies to be installed using a single step. @@ -121,6 +126,7 @@ jobs: run: npm ci - name: Build WordPress + timeout-minutes: 5 run: npm run build:dev - name: General debug information diff --git a/.github/workflows/reusable-upgrade-testing.yml b/.github/workflows/reusable-upgrade-testing.yml index 169abe20006a4..95645aee79981 100644 --- a/.github/workflows/reusable-upgrade-testing.yml +++ b/.github/workflows/reusable-upgrade-testing.yml @@ -56,6 +56,7 @@ jobs: # - Updates to the version of WordPress being tested. # - Updates the database. # - Checks the version of WordPress after the upgrade. + # - Serves the upgraded site and confirms it renders. upgrade-tests: name: PHP ${{ inputs.php }} with ${{ 'mariadb' == inputs.db-type && 'MariaDB' || 'MySQL' }} ${{ inputs.db-version }}${{ inputs.multisite && ' multisite' || '' }} permissions: @@ -63,13 +64,19 @@ jobs: runs-on: ${{ inputs.os }} timeout-minutes: 20 + env: + WP_ADMIN_USER: admin + WP_ADMIN_PASSWORD: password + services: database: image: ${{ inputs.db-type }}:${{ inputs.db-version }} ports: - 3306 + # Ping over TCP, to wait for the TCP server rather than the Unix socket. + # MariaDB 11.0 dropped the `mysqladmin` symlink, so pick the client the image ships. options: >- - --health-cmd="mysqladmin ping" + --health-cmd="${{ inputs.db-type == 'mariadb' && ! contains( fromJSON('["5.5", "10.0", "10.1", "10.2", "10.3"]'), inputs.db-version ) && 'mariadb-admin' || 'mysqladmin' }} ping -h 127.0.0.1 --protocol=tcp" --health-interval="30s" --health-timeout="10s" --health-retries="5" @@ -97,8 +104,8 @@ jobs: - name: Install WordPress run: | wp core ${{ inputs.multisite && 'multisite-install' || 'install' }} \ - --url=http://localhost/ --title="Upgrade Test" --admin_user=admin \ - --admin_password=password --admin_email=me@example.org --skip-email + --url=http://localhost/ --title="Upgrade Test" --admin_user="${WP_ADMIN_USER}" \ + --admin_password="${WP_ADMIN_PASSWORD}" --admin_email=me@example.org --skip-email - name: Pre-upgrade version check run: wp core version @@ -135,3 +142,11 @@ jobs: - name: Post-upgrade version check run: wp core version + + - name: Post-upgrade smoke check + # A network's domain lives in wp-config.php and the database, not just an option. + if: ${{ ! inputs.multisite }} + uses: WordPress/wordpress-develop/.github/actions/smoke-check@trunk # zizmor: ignore[unpinned-uses] + with: + admin-user: ${{ env.WP_ADMIN_USER }} + admin-password: ${{ env.WP_ADMIN_PASSWORD }} diff --git a/.github/workflows/reusable-workflow-lint.yml b/.github/workflows/reusable-workflow-lint.yml index 6ae6b0df10968..3c4e265458218 100644 --- a/.github/workflows/reusable-workflow-lint.yml +++ b/.github/workflows/reusable-workflow-lint.yml @@ -1,3 +1,8 @@ +## +# A reusable workflow that lints GitHub Actions workflow files. +# +# This workflow is used by `trunk` and branches >= 6.8. +## name: Lint GitHub Actions workflows on: workflow_call: @@ -29,6 +34,10 @@ jobs: with: args: "-color -verbose" + # actionlint shellchecks inline `run:` blocks, but does not read composite actions. + - name: Run shellcheck on composite action scripts + run: find .github -path '.github/actions/*' -name '*.sh' -print0 | xargs -0 -r shellcheck + # Runs the Zizmor GitHub Action workflow file linter. # # See https://github.com/zizmorcore/zizmor diff --git a/.github/workflows/slack-notifications.yml b/.github/workflows/slack-notifications.yml index 6a2f23714e291..0877405409e83 100644 --- a/.github/workflows/slack-notifications.yml +++ b/.github/workflows/slack-notifications.yml @@ -25,9 +25,6 @@ on: SLACK_GHA_FAILURE_WEBHOOK: description: 'The Slack webhook URL for a failed build.' required: true - SLACK_GHA_TIMEOUT_WEBHOOK: - description: 'The Slack webhook URL for a timed out build.' - required: false # Disable permissions for all available scopes by default. # Any needed permissions should be configured at the job level. @@ -58,7 +55,6 @@ jobs: if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event.workflow_run.event != 'pull_request' }} outputs: previous_conclusion: ${{ steps.previous-attempt-result.outputs.result }} - timed_out: ${{ steps.timeout-check.outputs.result }} payload: ${{ steps.create-payload.outputs.payload }} steps: @@ -132,25 +128,6 @@ jobs: env: CALLING_STATUS: ${{ inputs.calling_status }} - # The REST API exposes a distinct `timed_out` job conclusion that is unavailable through workflow contexts. - - name: Determine whether the workflow timed out - id: timeout-check - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - if: ${{ inputs.calling_status == 'cancelled' }} - with: - retries: 2 - retry-exempt-status-codes: 418 - result-encoding: string - script: | - const jobs = await github.paginate( github.rest.actions.listJobsForWorkflowRunAttempt, { - owner: context.repo.owner, - repo: context.repo.repo, - run_id: `${context.runId}`, - attempt_number: Number( process.env.GITHUB_RUN_ATTEMPT ), - }, ( response ) => response.data.jobs ); - - return jobs.some( ( job ) => job.conclusion === 'timed_out' ) ? 'true' : 'false'; - - name: Get the commit message id: current-commit-message uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -234,13 +211,14 @@ jobs: webhook: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} payload: ${{ needs.prepare.outputs.payload }} + # Posts notifications when a workflow is cancelled. cancelled: name: Cancelled notifications permissions: {} runs-on: ubuntu-24.04 timeout-minutes: 20 needs: [ prepare ] - if: ${{ ( inputs.calling_status == 'cancelled' && github.run_attempt == 2 && needs.prepare.outputs.timed_out != 'true' ) || cancelled() }} + if: ${{ inputs.calling_status == 'cancelled' && github.run_attempt == 2 || cancelled() }} steps: - name: Post cancelled notifications to Slack @@ -249,19 +227,3 @@ jobs: webhook-type: webhook-trigger webhook: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} payload: ${{ needs.prepare.outputs.payload }} - - timeout: - name: Timeout notifications - permissions: {} - runs-on: ubuntu-24.04 - timeout-minutes: 20 - needs: [ prepare ] - if: ${{ inputs.calling_status == 'cancelled' && github.run_attempt == 2 && needs.prepare.outputs.timed_out == 'true' }} - - steps: - - name: Post timeout notifications to Slack - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 - with: - webhook-type: webhook-trigger - webhook: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} - payload: ${{ needs.prepare.outputs.payload }} diff --git a/.github/workflows/test-and-zip-default-themes.yml b/.github/workflows/test-and-zip-default-themes.yml index 0464a1652d36b..d0ac2d43e0378 100644 --- a/.github/workflows/test-and-zip-default-themes.yml +++ b/.github/workflows/test-and-zip-default-themes.yml @@ -10,6 +10,7 @@ on: # Changing the preferred version of Node.js could affect themes with build processes. - '.npmrc' - '.nvmrc' + - '.github/actions/setup-npm/**' # Changes to any themes with a build script should be confirmed. - 'src/wp-content/themes/twentynineteen/**' - 'src/wp-content/themes/twentytwenty/**' @@ -27,6 +28,7 @@ on: # Changing the preferred version of Node.js could affect themes with build processes. - '.npmrc' - '.nvmrc' + - '.github/actions/setup-npm/**' # Changes to any themes with a build script should be confirmed. - 'src/wp-content/themes/twentynineteen/**' - 'src/wp-content/themes/twentytwenty/**' @@ -81,6 +83,7 @@ jobs: fail-fast: false matrix: theme: [ + 'ipsum', 'twentytwentyfive', 'twentytwentyfour', 'twentytwentythree', @@ -160,9 +163,15 @@ jobs: - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version-file: '.nvmrc' - cache: npm - cache-dependency-path: src/wp-content/themes/${{ matrix.theme }}/package-lock.json + node-version-file: 'src/wp-content/themes/${{ matrix.theme }}/.nvmrc' + package-manager-cache: false + + - name: Set up npm + # A workflow_dispatch run can check out a release branch, which lacks the action. + if: ${{ hashFiles('.github/actions/setup-npm/action.yml') != '' }} + uses: ./.github/actions/setup-npm + with: + working-directory: src/wp-content/themes/${{ matrix.theme }} - name: Install npm dependencies run: npm ci @@ -216,6 +225,7 @@ jobs: fail-fast: false matrix: theme: [ + 'ipsum', 'twentytwentyfive', 'twentytwentyfour', 'twentytwentythree', @@ -245,9 +255,15 @@ jobs: if: matrix.theme == 'twentytwentytwo' || matrix.theme == 'twentytwentyfive' uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version-file: '.nvmrc' - cache: npm - cache-dependency-path: src/wp-content/themes/${{ matrix.theme }}/package-lock.json + node-version-file: 'src/wp-content/themes/${{ matrix.theme }}/.nvmrc' + package-manager-cache: false + + - name: Set up npm + # A workflow_dispatch run can check out a release branch, which lacks the action. + if: ${{ ( matrix.theme == 'twentytwentytwo' || matrix.theme == 'twentytwentyfive' ) && hashFiles('.github/actions/setup-npm/action.yml') != '' }} + uses: ./.github/actions/setup-npm + with: + working-directory: src/wp-content/themes/${{ matrix.theme }} - name: Install npm dependencies if: matrix.theme == 'twentytwentytwo' || matrix.theme == 'twentytwentyfive' @@ -284,7 +300,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.github/workflows/test-build-processes.yml b/.github/workflows/test-build-processes.yml index ab3bea392cef8..ed970276e9833 100644 --- a/.github/workflows/test-build-processes.yml +++ b/.github/workflows/test-build-processes.yml @@ -12,23 +12,38 @@ on: - '3.[7-9]' - '[4-9].[0-9]' paths: - # Any change to a PHP, CSS, JavaScript, or JSON file should run checks. - - '**.css' - - '**.js' - - '**.json' - - '**.php' - # These files configure npm and the task runner. Changes could affect the outcome. - - 'package*.json' - - '.npmrc' - - '.nvmrc' - - 'Gruntfile.js' - - 'webpack.config.js' + - '**' + # Documentation, repository metadata, and local environment configuration. + - '!**.md' + - '!.devcontainer/**' + - '!.editorconfig' + - '!.env.example' + - '!.git-blame-ignore-revs' + - '!.gitignore' + - '!.mailmap' + - '!docker-compose.yml' + - '!typings/**' + - '!wp-cli.yml' + # Configuration for unrelated checks. + - '!.eslintrc-jsdoc.js' + - '!.prettierrc.js' + - '!.version-support-*.json' + - '!eslint.config.js' + - '!jsdoc.conf.json' + - '!phpcompat.xml.dist' + - '!phpcs.xml.dist' + - '!phpstan.neon.dist' + - '!phpunit.xml.dist' + - '!tsconfig.json' + # Tests, unrelated tools, and other workflows do not affect the build. + - '!tests/**' + - '!tools/**' + - '!.github/**' + # Re-include the tools and workflows used by the build. - 'tools/gutenberg/**' - 'tools/vendors/**' - 'tools/webpack/**' - # These files configure Composer. Changes could affect the outcome. - - 'composer.*' - # Confirm any changes to relevant workflow files. + - '.github/actions/setup-npm/**' - '.github/workflows/test-build-processes.yml' - '.github/workflows/reusable-test-core-build-process.yml' workflow_dispatch: @@ -118,7 +133,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index 94f57628254c3..c29be2fb4043e 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -6,6 +6,7 @@ on: branches: - trunk paths: + - '.github/actions/setup-npm/**' - '.github/workflows/test-coverage.yml' - '.github/workflows/reusable-phpunit-tests-v3.yml' - '.github/workflows/reusable-prepare-gutenberg.yml' @@ -19,6 +20,7 @@ on: branches: - trunk paths: + - '.github/actions/setup-npm/**' - '.github/workflows/test-coverage.yml' - '.github/workflows/reusable-phpunit-tests-v3.yml' - '.github/workflows/reusable-prepare-gutenberg.yml' @@ -97,7 +99,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.github/workflows/test-old-branches.yml b/.github/workflows/test-old-branches.yml index 7094550485c4f..37425c8b3983c 100644 --- a/.github/workflows/test-old-branches.yml +++ b/.github/workflows/test-old-branches.yml @@ -7,7 +7,7 @@ on: - trunk paths: - '.github/workflows/test-old-branches.yml' - - '.github/workflows/reusable-phpunit-tests-v[1-3].yml' + - '.github/workflows/reusable-*.yml' # Run twice a month on the 1st and 15th at 00:00 UTC. schedule: - cron: '0 0 1 * *' @@ -158,4 +158,3 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} diff --git a/.github/workflows/upgrade-develop-testing.yml b/.github/workflows/upgrade-develop-testing.yml index a01e5239fac17..eb9046a273ed5 100644 --- a/.github/workflows/upgrade-develop-testing.yml +++ b/.github/workflows/upgrade-develop-testing.yml @@ -12,7 +12,10 @@ on: paths: # Any change to a source PHP file should run checks. - 'src/**.php' + - '*.php' # PHP files in the root, like wp-config-sample.php. # Confirm any changes to relevant workflow files. + - '.github/actions/smoke-check/**' + - '.github/actions/setup-npm/**' - '.github/workflows/upgrade-develop-testing.yml' - '.github/workflows/reusable-upgrade-testing.yml' pull_request: @@ -23,9 +26,13 @@ on: paths: # Any change to a source PHP file should run checks. - 'src/**.php' + - '*.php' # PHP files in the root, like wp-config-sample.php. # Confirm any changes to relevant workflow files. + - '.github/actions/setup-npm/**' - '.github/workflows/upgrade-develop-testing.yml' - '.github/workflows/reusable-upgrade-testing.yml' + schedule: + - cron: '0 0 * * 1' workflow_dispatch: # Cancels all previous workflow runs for pull requests that have not completed. @@ -93,13 +100,14 @@ jobs: multisite: ${{ matrix.multisite }} # Run a limited set of upgrade tests for the current branch on forks. + # + # Pull requests meeting the filtering criteria run, but `push` and `schedule` configured runs are explicitly disabled + # for forks. upgrade-tests-develop-forks: name: Upgrade from ${{ matrix.wp }} uses: ./.github/workflows/reusable-upgrade-testing.yml - # This job also runs for the push event, which has no draft state to check. if: | - github.repository != 'WordPress/wordpress-develop' && ( - github.event_name != 'pull_request' || + github.repository != 'WordPress/wordpress-develop' && github.event_name == 'pull_request' && ( ! github.event.repository.private || ! github.event.pull_request.draft || contains( github.event.pull_request.labels.*.name, 'Draft Workflow Runs' ) @@ -140,7 +148,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.github/workflows/upgrade-testing.yml b/.github/workflows/upgrade-testing.yml index ba82f96a7b779..d25bfb7271615 100644 --- a/.github/workflows/upgrade-testing.yml +++ b/.github/workflows/upgrade-testing.yml @@ -9,6 +9,7 @@ on: - trunk # Always test the workflow after it's updated. paths: + - '.github/actions/smoke-check/**' - '.github/workflows/upgrade-testing.yml' - '.github/workflows/reusable-upgrade-testing.yml' pull_request: @@ -57,11 +58,102 @@ permissions: {} # - 5.6.x Docker containers are available and work, but 5.6 only accounts for ~2.3% of installs as of 12/6/2024.defaults: # - 5.7.x accounts for ~20% of installs, so this is used below instead. jobs: + # Builds the list of starting versions for each matrix below. Drops every version that is not older than the target + # version, because upgrading from one of those is a downgrade. A job whose list ends up empty is skipped. + # + # A major target (6.4 or 6.4.0) drops its own branch, because no upgrade would occur. + # + # A minor target (6.4.4) keeps its own branch, because 6.4 to 6.4.4 is a real upgrade. The called workflow runs + # `wp core update --minor` before the final upgrade step, so the minor update performs that upgrade and the final + # upgrade step is a no-op. When the target is not the newest release on its branch, the minor update overshoots + # the target and the target is never installed. + # + # "latest" and "nightly" drop nothing. Any other unrecognized target is treated the same way, with a warning. + build-test-matrix: + name: Build Test Matrix + runs-on: ubuntu-24.04 + permissions: {} + if: ${{ github.repository == 'WordPress/wordpress-develop' }} + timeout-minutes: 5 + outputs: + recent-releases: ${{ steps.versions.outputs.recent-releases }} + wp-6x: ${{ steps.versions.outputs.wp-6x }} + wp-5x-php-7x: ${{ steps.versions.outputs.wp-5x-php-7x }} + wp-5x-php-8x: ${{ steps.versions.outputs.wp-5x-php-8x }} + oldest-wp: ${{ steps.versions.outputs.oldest-wp }} + + steps: + - name: Drop the versions that are not older than the version being tested + id: versions + env: + NEW_VERSION: ${{ inputs.new-version && inputs.new-version || 'latest' }} + # The versions each job below starts from. See the comment on the job for why those versions are tested. + RECENT_RELEASES: '7.0 7.1' # upgrade-tests-recent-releases + WP_6X: '6.0 6.3 6.4 6.5' # upgrade-tests-wp-6x-mysql + WP_5X_PHP_7X: '5.0 5.1 5.3 5.4 5.5 5.6 5.9' # upgrade-tests-wp-5x-php-7x-mysql + WP_5X_PHP_8X: '5.3 5.4 5.5 5.6 5.9' # upgrade-tests-wp-5x-php-8x-mysql + OLDEST_WP: '4.7' # upgrade-tests-oldest-wp-mysql + run: | + # Branches are compared as a single number so that 7.0 sorts above 6.9. The spacing leaves room for a + # pre-release to sort between its own branch and the one before it. The base is forced to 10 so that a + # version number with a leading zero is not read as an invalid octal value. + if [[ "$NEW_VERSION" =~ ^v?([0-9]+)\.([0-9]+)(\.([0-9]+))?$ ]]; then + target=$(( 10#${BASH_REMATCH[1]} * 10000 + 10#${BASH_REMATCH[2]} * 100 )) + + # A version with no patch number, or a patch number of 0, is a major release: drop its own branch too. + if [[ -z "${BASH_REMATCH[4]}" || "${BASH_REMATCH[4]}" == "0" ]]; then + target=$(( target - 1 )) + fi + else + # Sorts above every version in the lists above, so nothing is dropped. + target=999999 + + if [[ "$NEW_VERSION" != 'latest' && "$NEW_VERSION" != 'nightly' ]]; then + echo "::warning::\"${NEW_VERSION}\" was not read as a version number, so every version will be tested." + fi + fi + + # Writes the versions of $2 that are older than the target to the $1 output, as a JSON array. + filter() { + local kept='' wp branch + local -a versions + + # Guards against a list being renamed in the environment above without updating the call below, which + # would otherwise skip the whole job in silence. + [[ -n "$2" ]] || { echo "::error::No versions are listed for the ${1} matrix."; exit 1; } + + read -ra versions <<< "$2" + + for wp in "${versions[@]}"; do + [[ "$wp" =~ ^([0-9]+)\.([0-9]+) ]] || continue + branch=$(( 10#${BASH_REMATCH[1]} * 10000 + 10#${BASH_REMATCH[2]} * 100 )) + + # A pre-release sorts just below its own branch, so 7.1-RC2 is tested against a target of 7.1 but + # not against 7.0.1. + if [[ "$wp" == *-* ]]; then + branch=$(( branch - 1 )) + fi + + if (( branch <= target )); then + kept+="\"${wp}\"," + fi + done + + echo "$1=[${kept%,}]" >> "$GITHUB_OUTPUT" + } + + filter recent-releases "$RECENT_RELEASES" + filter wp-6x "$WP_6X" + filter wp-5x-php-7x "$WP_5X_PHP_7X" + filter wp-5x-php-8x "$WP_5X_PHP_8X" + filter oldest-wp "$OLDEST_WP" + # Tests the full list of PHP/MySQL combinations for the two most recent versions of WordPress. upgrade-tests-recent-releases: name: ${{ matrix.wp }} to ${{ inputs.new-version && inputs.new-version || 'latest' }} uses: ./.github/workflows/reusable-upgrade-testing.yml - if: ${{ github.repository == 'WordPress/wordpress-develop' }} + if: ${{ github.repository == 'WordPress/wordpress-develop' && needs.build-test-matrix.outputs.recent-releases != '[]' }} + needs: [ build-test-matrix ] permissions: contents: read strategy: @@ -71,7 +163,7 @@ jobs: php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] db-type: [ 'mysql' ] db-version: [ '5.7', '8.0', '8.4', '9.7' ] - wp: [ '7.0', '7.1' ] + wp: ${{ fromJSON( needs.build-test-matrix.outputs.recent-releases ) }} multisite: [ false, true ] with: os: ${{ matrix.os }} @@ -86,7 +178,8 @@ jobs: upgrade-tests-wp-6x-mysql: name: ${{ matrix.wp }} to ${{ inputs.new-version && inputs.new-version || 'latest' }} uses: ./.github/workflows/reusable-upgrade-testing.yml - if: ${{ github.repository == 'WordPress/wordpress-develop' }} + if: ${{ github.repository == 'WordPress/wordpress-develop' && needs.build-test-matrix.outputs.wp-6x != '[]' }} + needs: [ build-test-matrix ] permissions: contents: read strategy: @@ -96,7 +189,7 @@ jobs: php: [ '7.4', '8.0', '8.4' ] db-type: [ 'mysql' ] db-version: [ '5.7', '8.4' ] - wp: [ '6.0', '6.3', '6.4', '6.5' ] + wp: ${{ fromJSON( needs.build-test-matrix.outputs.wp-6x ) }} multisite: [ false, true ] with: os: ${{ matrix.os }} @@ -111,7 +204,8 @@ jobs: upgrade-tests-wp-5x-php-7x-mysql: name: ${{ matrix.wp }} to ${{ inputs.new-version && inputs.new-version || 'latest' }} uses: ./.github/workflows/reusable-upgrade-testing.yml - if: ${{ github.repository == 'WordPress/wordpress-develop' }} + if: ${{ github.repository == 'WordPress/wordpress-develop' && needs.build-test-matrix.outputs.wp-5x-php-7x != '[]' }} + needs: [ build-test-matrix ] permissions: contents: read strategy: @@ -121,7 +215,7 @@ jobs: php: [ '7.4' ] db-type: [ 'mysql' ] db-version: [ '5.7', '8.4' ] - wp: [ '5.0', '5.1', '5.3', '5.4', '5.5', '5.6', '5.9' ] + wp: ${{ fromJSON( needs.build-test-matrix.outputs.wp-5x-php-7x ) }} multisite: [ false, true ] with: os: ${{ matrix.os }} @@ -140,7 +234,8 @@ jobs: upgrade-tests-wp-5x-php-8x-mysql: name: ${{ matrix.wp }} to ${{ inputs.new-version && inputs.new-version || 'latest' }} uses: ./.github/workflows/reusable-upgrade-testing.yml - if: ${{ github.repository == 'WordPress/wordpress-develop' }} + if: ${{ github.repository == 'WordPress/wordpress-develop' && needs.build-test-matrix.outputs.wp-5x-php-8x != '[]' }} + needs: [ build-test-matrix ] permissions: contents: read strategy: @@ -150,7 +245,7 @@ jobs: php: [ '8.0', '8.4' ] db-type: [ 'mysql' ] db-version: [ '5.7', '8.4' ] - wp: [ '5.3', '5.4', '5.5', '5.6', '5.9' ] + wp: ${{ fromJSON( needs.build-test-matrix.outputs.wp-5x-php-8x ) }} multisite: [ false, true ] with: os: ${{ matrix.os }} @@ -170,7 +265,8 @@ jobs: upgrade-tests-oldest-wp-mysql: name: ${{ matrix.wp }} to ${{ inputs.new-version && inputs.new-version || 'latest' }} uses: ./.github/workflows/reusable-upgrade-testing.yml - if: ${{ github.repository == 'WordPress/wordpress-develop' }} + if: ${{ github.repository == 'WordPress/wordpress-develop' && needs.build-test-matrix.outputs.oldest-wp != '[]' }} + needs: [ build-test-matrix ] permissions: contents: read strategy: @@ -180,7 +276,7 @@ jobs: php: [ '7.4' ] db-type: [ 'mysql' ] db-version: [ '5.7', '8.0', '8.4', '9.7' ] - wp: [ '4.7' ] + wp: ${{ fromJSON( needs.build-test-matrix.outputs.oldest-wp ) }} multisite: [ false, true ] with: os: ${{ matrix.os }} @@ -197,7 +293,7 @@ jobs: permissions: actions: read contents: read - needs: [ upgrade-tests-recent-releases, upgrade-tests-wp-6x-mysql, upgrade-tests-wp-5x-php-7x-mysql, upgrade-tests-wp-5x-php-8x-mysql, upgrade-tests-oldest-wp-mysql ] + needs: [ build-test-matrix, upgrade-tests-recent-releases, upgrade-tests-wp-6x-mysql, upgrade-tests-wp-5x-php-7x-mysql, upgrade-tests-wp-5x-php-8x-mysql, upgrade-tests-oldest-wp-mysql ] if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} with: calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} @@ -206,7 +302,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 1ef050f1b9b92..315cb833c2b18 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -8,6 +8,7 @@ on: - '[7-9].[0-9]' paths: # Only run when changes are made to workflow files. + - '.github/actions/**' - '.github/workflows/**' pull_request: branches: @@ -15,6 +16,7 @@ on: - '[0-9].[0-9]' paths: # Only run when changes are made to workflow files. + - '.github/actions/**' - '.github/workflows/**' workflow_dispatch: @@ -61,7 +63,6 @@ jobs: SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - SLACK_GHA_TIMEOUT_WEBHOOK: ${{ secrets.SLACK_GHA_TIMEOUT_WEBHOOK }} failed-workflow: name: Failed workflow tasks diff --git a/.gitignore b/.gitignore index 15876fa47fee8..f96e379fbb304 100644 --- a/.gitignore +++ b/.gitignore @@ -72,6 +72,7 @@ wp-tests-config.php !/src/wp-content/themes/twentytwentythree !/src/wp-content/themes/twentytwentyfour !/src/wp-content/themes/twentytwentyfive +!/src/wp-content/themes/ipsum /src/wp-content/upgrade /src/wp-content/uploads /src/wp-content/advanced-cache.php diff --git a/.nvmrc b/.nvmrc index 209e3ef4b6247..82d5d714347ef 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +24.18 diff --git a/Gruntfile.js b/Gruntfile.js index ab40643d05a80..f96eda8ce5f61 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,5 +1,4 @@ /* jshint node:true */ -/* eslint-env es6 */ /* globals Set */ var webpackConfig = require( './webpack.config' ); var installChanged = require( 'install-changed' ); @@ -26,6 +25,7 @@ module.exports = function(grunt) { 'wp-content/index.php', 'wp-content/themes/index.php', 'wp-content/themes/twenty*/**', + 'wp-content/themes/ipsum/**', 'wp-content/plugins/index.php', 'wp-content/plugins/hello.php', 'wp-content/plugins/akismet/**', @@ -138,7 +138,6 @@ module.exports = function(grunt) { 'concat', 'copy', 'cssmin', - 'imagemin', 'jshint', 'uglify', 'watch' @@ -155,7 +154,11 @@ module.exports = function(grunt) { ] }; - // Load grunt-* tasks. + /** + * Loads the Grunt tasks for the given dependency. + * + * @param {string} dependency The name of the Grunt task to load. + */ function loadGruntTasks( dependency ) { var contrib = key === 'contrib' ? 'contrib-' : ''; grunt.loadNpmTasks( 'grunt-' + contrib + dependency ); @@ -368,6 +371,8 @@ module.exports = function(grunt) { [ WORKING_DIR + 'wp-includes/js/imagesloaded.min.js' ]: [ './node_modules/imagesloaded/imagesloaded.pkgd.min.js' ], [ WORKING_DIR + 'wp-includes/js/jquery/jquery.js' ]: [ './node_modules/jquery/dist/jquery.js' ], [ WORKING_DIR + 'wp-includes/js/jquery/jquery.min.js' ]: [ './node_modules/jquery/dist/jquery.min.js' ], + [ WORKING_DIR + 'wp-includes/js/jquery/jquery-migrate.js' ]: [ './node_modules/jquery-migrate/dist/jquery-migrate.js' ], + [ WORKING_DIR + 'wp-includes/js/jquery/jquery-migrate.min.js' ]: [ './node_modules/jquery-migrate/dist/jquery-migrate.min.js' ], [ WORKING_DIR + 'wp-includes/js/jquery/jquery.form.js' ]: [ './node_modules/jquery-form/src/jquery.form.js' ], [ WORKING_DIR + 'wp-includes/js/jquery/jquery.color.min.js' ]: [ './node_modules/jquery-color/dist/jquery.color.min.js' ], [ WORKING_DIR + 'wp-includes/js/masonry.min.js' ]: [ './node_modules/masonry-layout/dist/masonry.pkgd.min.js' ], @@ -527,6 +532,9 @@ module.exports = function(grunt) { [ WORKING_DIR + 'wp-admin/js/language-chooser.js' ]: [ './src/js/_enqueues/lib/language-chooser.js' ], [ WORKING_DIR + 'wp-admin/js/link.js' ]: [ './src/js/_enqueues/admin/link.js' ], [ WORKING_DIR + 'wp-admin/js/media-gallery.js' ]: [ './src/js/_enqueues/deprecated/media-gallery.js' ], + [ WORKING_DIR + 'wp-admin/js/media-frame-upload.js' ]: [ './src/js/_enqueues/admin/media-frame-upload.js' ], + [ WORKING_DIR + 'wp-admin/js/media-new-upload.js' ]: [ './src/js/_enqueues/admin/media-new-upload.js' ], + [ WORKING_DIR + 'wp-admin/js/media-upload-pipeline.js' ]: [ './src/js/_enqueues/admin/media-upload-pipeline.js' ], [ WORKING_DIR + 'wp-admin/js/media-upload.js' ]: [ './src/js/_enqueues/admin/media-upload.js' ], [ WORKING_DIR + 'wp-admin/js/media.js' ]: [ './src/js/_enqueues/admin/media.js' ], [ WORKING_DIR + 'wp-admin/js/nav-menu.js' ]: [ './src/js/_enqueues/lib/nav-menu.js' ], @@ -693,7 +701,7 @@ module.exports = function(grunt) { src: 'vendor/composer/ca-bundle/res/cacert.pem', dest: SOURCE_DIR + 'wp-includes/certificates/ca-bundle.crt' }, - // Gutenberg PHP infrastructure files (routes.php, pages.php, constants.php, pages/). + // Gutenberg PHP infrastructure files (routes.php, pages.php, constants.php, pages/, core actions, core fields). 'gutenberg-php': { options: { process: function( content ) { @@ -712,6 +720,10 @@ module.exports = function(grunt) { 'pages.php', 'constants.php', 'pages/**/*.php', + // The actions Core registers through the entity actions API. + 'scripts/core-actions/**/*.php', + // The fields Core registers through the entity fields API. + 'scripts/core-fields/**/*.php', ], dest: SOURCE_DIR + 'wp-includes/build/', } ], @@ -807,8 +819,8 @@ module.exports = function(grunt) { // Strip the 'library/' prefix from filePath values so they // resolve correctly relative to wp-includes/images/icon-library/. .replace( - /'filePath' => 'library\//g, - '\'filePath\' => \'' + /'filePath'(\s+)=> 'library\//g, + '\'filePath\'$1=> \'' ); } }, @@ -926,6 +938,11 @@ module.exports = function(grunt) { { expr: /content/im, action: function( prop, value ) { + // Alternative text, as in `content: "\f141" / '';`, is not part of the icon. + var altText = ( value.match( /\s*\/\s*(?:'[^']*'|"[^"]*")\s*$/ ) || [ '' ] )[ 0 ]; + + value = value.slice( 0, value.length - altText.length ); + if ( value === '"\\f141"' ) { // dashicons-arrow-left value = '"\\f139"'; } else if ( value === '"\\f340"' ) { // dashicons-arrow-left-alt @@ -939,7 +956,7 @@ module.exports = function(grunt) { } else if ( value === '"\\f345"' ) { // dashicons-arrow-right-alt2 value = '"\\f341"'; } - return { prop: prop, value: value }; + return { prop: prop, value: value + altText }; } } ] @@ -1069,28 +1086,70 @@ module.exports = function(grunt) { '**/*.js', '!**/*.min.js' ], - /* - * Limit JSHint's run to a single specified plugin directory: + // Prevent traversal into these directories during glob expansion. + // This is much faster than using negation patterns alone. + ignore: [ + '**/build/**', + '**/dist/**', + '**/gutenberg/**', + '**/node_modules/**', + '**/packages/**', + '**/test/**', + '**/vendor/**' + ], + /** + * Limits JSHint's run to a single specified plugin directory: + * + * Usage example: + * grunt jshint:plugins --dir=foldername + * + * This also automatically skips minified files that may not have + * a filename ending with `.min.js`, specifically when they have + * lines longer than 500 characters. * - * grunt jshint:plugins --dir=foldername + * @param {string} dirpath Directory path. + * @return {boolean} Whether the path is skipped. */ filter: function( dirpath ) { - var index, dir = grunt.option( 'dir' ); + // Bypasses folder targets so fs.readFileSync doesn't throw errors. + if ( ! fs.lstatSync( dirpath ).isFile() ) { + return false; + } - // Don't filter when no target folder is specified. - if ( ! dir ) { - return true; + // If `--dir` is provided. + var dir = grunt.option( 'dir' ); + if ( dir ) { + var normalizedPath = dirpath.replace( /\\/g, '/' ); + var index = normalizedPath.lastIndexOf( '/' + dir ); + + // If a directory was requested but this file isn't in it, exclude it. + if ( -1 === index ) { + return false; + } } - dirpath = dirpath.replace( /\\/g, '/' ); - index = dirpath.lastIndexOf( '/' + dir ); + // Read the file and inspect line lengths to catch minified/bundled code. + var content; + try { + content = fs.readFileSync( dirpath, 'utf-8' ); + } catch ( error ) { + grunt.log.writeln( 'Could not read file: ' + dirpath + ' (Error: ' + error.message + ')' ); + return false; + } - // Match only the folder name passed from cli. - if ( -1 !== index ) { - return true; + var lines = content.split( '\n' ); + + // Cap the maximum number of iterations at 5 lines. + for ( const line of lines.slice( 0, 5 ) ) { + // Exclude files with lines longer than 500 characters. + if ( line.length > 500 ) { + grunt.log.writeln( 'Skipping minified file: ' + dirpath ); + return false; + } } - return false; + // File passed both independent checks: proceed with JSHint. + return true; } } }, @@ -1270,6 +1329,9 @@ module.exports = function(grunt) { 'src/wp-admin/js/language-chooser.js': 'src/js/_enqueues/lib/language-chooser.js', 'src/wp-admin/js/link.js': 'src/js/_enqueues/admin/link.js', 'src/wp-admin/js/media-gallery.js': 'src/js/_enqueues/deprecated/media-gallery.js', + 'src/wp-admin/js/media-frame-upload.js': 'src/js/_enqueues/admin/media-frame-upload.js', + 'src/wp-admin/js/media-new-upload.js': 'src/js/_enqueues/admin/media-new-upload.js', + 'src/wp-admin/js/media-upload-pipeline.js': 'src/js/_enqueues/admin/media-upload-pipeline.js', 'src/wp-admin/js/media-upload.js': 'src/js/_enqueues/admin/media-upload.js', 'src/wp-admin/js/media.js': 'src/js/_enqueues/admin/media.js', 'src/wp-admin/js/nav-menu.js': 'src/js/_enqueues/lib/nav-menu.js', @@ -1552,6 +1614,15 @@ module.exports = function(grunt) { ], dest: BUILD_DIR + 'wp-includes/js/' }, + { + expand: true, + cwd: BUILD_DIR + 'wp-includes/js/jquery', + src: [ + 'jquery-migrate.js', + 'jquery-migrate.min.js' + ], + dest: BUILD_DIR + 'wp-includes/js/jquery/' + }, { expand: true, cwd: BUILD_DIR + 'wp-includes/js/dist/', @@ -1645,7 +1716,7 @@ module.exports = function(grunt) { if( grunt.option( 'minimal-copy' ) ) { var copyFilesOptions = grunt.config.get( 'copy.files.files' ); copyFilesOptions[0].src.push( '!wp-content/plugins/**' ); - copyFilesOptions[0].src.push( '!wp-content/themes/!(twenty*)/**' ); + copyFilesOptions[0].src.push( '!wp-content/themes/!(twenty*|ipsum)/**' ); grunt.config.set( 'copy.files.files', copyFilesOptions ); } @@ -1656,13 +1727,13 @@ module.exports = function(grunt) { grunt.loadNpmTasks( 'grunt-webpack' ); // RTL task. - grunt.registerTask('rtl', ['rtlcss:core', 'rtlcss:colors']); + grunt.registerTask('rtl', 'Generates right-to-left (RTL) stylesheets for core and the admin color schemes.', ['rtlcss:core', 'rtlcss:colors']); // Color schemes task. - grunt.registerTask('colors', ['sass:colors', 'postcss:colors']); + grunt.registerTask('colors', 'Compiles and processes the admin color scheme stylesheets.', ['sass:colors', 'postcss:colors']); // JSHint task. - grunt.registerTask( 'jshint:corejs', [ + grunt.registerTask( 'jshint:corejs', 'Runs JSHint on the Gruntfile, tests, bundled themes, and core JavaScript files.', [ 'jshint:grunt', 'jshint:tests', 'jshint:themes', @@ -1670,7 +1741,7 @@ module.exports = function(grunt) { 'jshint:media' ] ); - grunt.registerTask( 'restapi-jsclient', [ + grunt.registerTask( 'restapi-jsclient', 'Runs the PHPUnit and QUnit tests for the REST API JavaScript client.', [ 'phpunit:restapi-jsclient', 'qunit:compiled' ] ); @@ -1729,7 +1800,7 @@ module.exports = function(grunt) { grunt.renameTask( 'watch', '_watch' ); - grunt.registerTask( 'watch', function() { + grunt.registerTask( 'watch', 'Watches files for changes and runs the related tasks. Use watch:phpunit or --phpunit to also run PHPUnit tests.', function() { if ( ! this.args.length || this.args.indexOf( 'webpack' ) > -1 ) { grunt.task.run( 'build' ); } @@ -1744,13 +1815,16 @@ module.exports = function(grunt) { grunt.task.run( '_' + this.nameArgs ); } ); - grunt.registerTask( 'precommit:image', [ + grunt.registerMultiTask( 'imagemin', 'Losslessly optimizes PNG, JPEG and GIF images.', require( './tools/imagemin/task.js' )( grunt ) ); + + grunt.registerTask( 'precommit:image', 'Optimizes core images in preparation for a commit.', [ 'imagemin:core' ] ); - grunt.registerTask( 'precommit:js', [ + grunt.registerTask( 'precommit:js', 'Builds, lints, type checks, and tests JavaScript files in preparation for a commit.', [ 'webpack:prod', 'jshint:corejs', + 'lint:jsdoc', 'typecheck:js', 'uglify:imgareaselect', 'uglify:jqueryform', @@ -1760,16 +1834,16 @@ module.exports = function(grunt) { 'qunit:compiled' ] ); - grunt.registerTask( 'precommit:css', [ + grunt.registerTask( 'precommit:css', 'Processes core CSS files with PostCSS in preparation for a commit.', [ 'postcss:core' ] ); - grunt.registerTask( 'precommit:php', [ + grunt.registerTask( 'precommit:php', 'Runs PHPStan and PHPUnit tests in preparation for a commit.', [ 'phpstan', 'phpunit' ] ); - grunt.registerTask( 'precommit:emoji', [ + grunt.registerTask( 'precommit:emoji', 'Updates the emoji regular expressions from the latest Twemoji file list. Requires the GitHub CLI.', [ 'replace:emoji-regex' ] ); @@ -1786,6 +1860,11 @@ module.exports = function(grunt) { path.dirname( __dirname ) + '/.svn' ] ); + /** + * Searches for the first version control directory in the given set. + * + * @param {string[]} set Array of directory paths to check. + */ function find( set ) { var dir; @@ -1798,6 +1877,9 @@ module.exports = function(grunt) { } } + /** + * Runs all tasks. + */ function runAllTasks() { grunt.log.writeln( 'Cannot determine which files are modified as SVN and GIT are not available.' ); grunt.log.writeln( 'Running all tasks and all tests.' ); @@ -1813,6 +1895,11 @@ module.exports = function(grunt) { done(); } + /** + * Determines which precommit tasks to run based on modified files detected by version control. + * + * @param {string} type The version control type: 'git' or 'svn'. + */ function run( type ) { var command = map[ type ].split( ' ' ); @@ -1822,13 +1909,23 @@ module.exports = function(grunt) { }, function( error, result, code ) { var taskList = []; - // Callback for finding modified paths. + /** + * Checks if the given path appears in the version control status output. + * + * @param {string} path The path to check. + * @return {boolean} True if the path is found, false otherwise. + */ function testPath( path ) { var regex = new RegExp( ' ' + path + '$', 'm' ); return regex.test( result.stdout ); } - // Callback for finding modified files by extension. + /** + * Checks if files with the given extension appear in the version control status output. + * + * @param {string} extension The file extension to check for. + * @return {boolean} True if the file with the given extension is found, false otherwise. + */ function testExtension( extension ) { var regex = new RegExp( '\.' + extension + '$', 'm' ); return regex.test( result.stdout ); @@ -1886,14 +1983,14 @@ module.exports = function(grunt) { } } ); - grunt.registerTask( 'copy:js', [ + grunt.registerTask( 'copy:js', 'Copies JavaScript files from npm packages, vendor libraries, wp-admin, and wp-includes.', [ 'copy:npm-packages', 'copy:vendor-js', 'copy:admin-js', 'copy:includes-js' ] ); - grunt.registerTask( 'uglify:all', [ + grunt.registerTask( 'uglify:all', 'Minifies all JavaScript files.', [ 'uglify:core', 'uglify:emoji-loader', 'uglify:jquery-ui', @@ -1904,20 +2001,20 @@ module.exports = function(grunt) { 'uglify:wp-polyfill-fetch' ] ); - grunt.registerTask( 'build:codemirror', [ + grunt.registerTask( 'build:codemirror', 'Builds the CodeMirror script and styles.', [ 'webpack:codemirror', 'cssmin:codemirror', 'usebanner:codemirror', 'copy:codemirror' ] ); - grunt.registerTask( 'build:webpack', [ + grunt.registerTask( 'build:webpack', 'Cleans the webpack assets and runs the production and development webpack builds.', [ 'clean:webpack-assets', 'webpack:prod', 'webpack:dev', ] ); - grunt.registerTask( 'build:js', [ + grunt.registerTask( 'build:js', 'Builds all JavaScript files.', [ 'clean:js', 'build:webpack', 'copy:js', @@ -1927,7 +2024,7 @@ module.exports = function(grunt) { 'concat:emoji' ] ); - grunt.registerTask( 'build:css', [ + grunt.registerTask( 'build:css', 'Builds all CSS files, including the RTL and admin color scheme stylesheets.', [ 'clean:css', 'copy:wp-admin-css-compat-rtl', 'copy:wp-admin-css-compat-min', @@ -2018,30 +2115,30 @@ module.exports = function(grunt) { } ); } ); - grunt.registerTask( 'build:certificates', [ + grunt.registerTask( 'build:certificates', 'Copies the certificate authority bundle into wp-includes.', [ 'copy:certificates' ] ); - grunt.registerTask( 'certificates:upgrade', [ + grunt.registerTask( 'certificates:upgrade', 'Upgrades the certificate authority bundle package and copies the bundle into wp-includes.', [ 'certificates:upgrade-package', 'copy:certificates' ] ); - grunt.registerTask( 'build:files', [ + grunt.registerTask( 'build:files', 'Cleans the build directory and copies the core files into it.', [ 'clean:files', 'copy:files', 'copy:version', ] ); - grunt.registerTask( 'replace:workflow-references-local-to-remote', [ + grunt.registerTask( 'replace:workflow-references-local-to-remote', 'Replaces local reusable workflow references with references to trunk.', [ 'copy:workflow-references-local-to-remote', ]); - grunt.registerTask( 'replace:workflow-references-remote-to-local', [ + grunt.registerTask( 'replace:workflow-references-remote-to-local', 'Replaces reusable workflow references to trunk with local references.', [ 'copy:workflow-references-remote-to-local', ]); - grunt.registerTask( 'post-branching', [ + grunt.registerTask( 'post-branching', 'Removes unneeded workflows and points reusable workflow references to trunk after branching.', [ 'clean:workflows', 'replace:workflow-references-local-to-remote' ]); @@ -2049,7 +2146,7 @@ module.exports = function(grunt) { /** * Build verification tasks. */ - grunt.registerTask( 'verify:build', [ + grunt.registerTask( 'verify:build', 'Runs all build verification tasks.', [ 'verify:old-files', 'verify:source-maps', ] ); @@ -2059,7 +2156,7 @@ module.exports = function(grunt) { * * @ticket 36083 */ - grunt.registerTask( 'verify:old-files', function() { + grunt.registerTask( 'verify:old-files', 'Verifies that no files listed in $_old_files exist in the build directory.', function() { const file = `${ BUILD_DIR }wp-admin/includes/update-core.php`; assert( @@ -2119,7 +2216,7 @@ module.exports = function(grunt) { * @ticket 46218 * @ticket 60348 */ - grunt.registerTask( 'verify:source-maps', function() { + grunt.registerTask( 'verify:source-maps', 'Verifies that built JavaScript files do not contain source map links.', function() { const ignoredFiles = [ 'build/wp-includes/js/dist/components.js', 'build/wp-includes/js/dist/data.js', @@ -2205,7 +2302,7 @@ module.exports = function(grunt) { } ); // Detects and copies stable routes. - grunt.registerTask( 'build:routes', [ + grunt.registerTask( 'build:routes', 'Detects and copies the stable Gutenberg routes.', [ 'routes:setup', 'copy:routes', ] ); @@ -2217,7 +2314,7 @@ module.exports = function(grunt) { * * Because all of these tasks write to src/, the outcome is identical for build and build:dev. */ - grunt.registerTask( 'build:gutenberg', [ + grunt.registerTask( 'build:gutenberg', 'Refreshes the Gutenberg-sourced content in src/.', [ 'clean:gutenberg', 'copy:gutenberg-php', 'build:routes', @@ -2230,7 +2327,7 @@ module.exports = function(grunt) { 'copy:icon-library-manifest', ] ); - grunt.registerTask( 'build', function() { + grunt.registerTask( 'build', 'Builds WordPress. Use --dev to build into src/ for development.', function() { if ( grunt.option( 'dev' ) ) { grunt.task.run( [ 'gutenberg:verify', @@ -2255,7 +2352,7 @@ module.exports = function(grunt) { } } ); - grunt.registerTask( 'prerelease', [ + grunt.registerTask( 'prerelease', 'Runs formatting checks, linting, tests, and image optimization in preparation for a release.', [ 'format:php:error', 'precommit:php', 'precommit:js', @@ -2308,6 +2405,18 @@ module.exports = function(grunt) { } ); } ); + grunt.registerTask( 'lint:jsdoc', 'Runs JSDoc linting on JavaScript files.', function() { + var done = this.async(); + + grunt.util.spawn( { + cmd: 'npm', + args: [ 'run', 'lint:jsdoc' ], + opts: { stdio: 'inherit' } + }, function( error ) { + done( ! error ); + } ); + } ); + grunt.registerTask( 'phpstan', 'Runs PHPStan on the entire codebase.', function() { var done = this.async(); @@ -2384,7 +2493,7 @@ module.exports = function(grunt) { grunt.registerTask('apply', 'patch'); // Default task. - grunt.registerTask('default', ['build']); + grunt.registerTask('default', 'Runs the build task.', ['build']); /* * Automatically updates the `:dynamic` configurations diff --git a/README.md b/README.md index eb88e407cb612..cb64a35538457 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ You will need Node and npm installed on your computer. Node is a JavaScript runt If you are not using a package manager, see the [Node.js download page](https://nodejs.org/en/download/) for installers and binaries. -**Note:** WordPress currently only officially supports Node.js `20.x` and npm `10.x`. +**Note:** WordPress currently only officially supports Node.js `24.x` and npm `11.x`. You will also need a container environment such as [Docker Desktop](https://www.docker.com/products/docker-desktop) installed and running on your computer. The container environment is the virtualization software that powers the local development environment and can be installed just like any other regular application. diff --git a/composer.json b/composer.json index 34a8d980259f1..abc5335126f07 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "ext-igbinary": "*", "ext-imagick": "*", "ext-intl": "*", + "ext-libxml": "*", "ext-mbstring": "*", "ext-memcached": "*", "ext-mysqli": "*", @@ -47,16 +48,21 @@ "ext-zlib": "*" }, "require-dev": { - "composer/ca-bundle": "1.5.13", + "composer/ca-bundle": "1.6.0", "squizlabs/php_codesniffer": "3.13.5", "wp-coding-standards/wpcs": "~3.4.1", "phpcompatibility/phpcompatibility-wp": "~2.1.3", - "phpstan/phpstan": "2.2.5", - "phpstan/phpstan-phpunit": "2.0.18", + "phpstan/phpstan": "2.3.1", + "phpstan/phpstan-phpunit": "2.1.1", "yoast/phpunit-polyfills": "^1.1.0" }, "config": { "audit": { "ignore": [ "GHSA-hmqg-cxww-wqhq", "PKSA-rdkp-vv9z-mjkg" ] }, + "policy": { + "advisories": { + "ignore-id": [ "GHSA-hmqg-cxww-wqhq", "PKSA-rdkp-vv9z-mjkg" ] + } + }, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true }, diff --git a/docker-compose.yml b/docker-compose.yml index 02cd542c245db..61a123f7427c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,13 +15,14 @@ services: environment: LOCAL_DIR: ${LOCAL_DIR-src} LOCAL_NGINX_COMPRESSION: ${LOCAL_NGINX_COMPRESSION:-off} + LOCAL_NGINX_STATIC_EXPIRES: ${LOCAL_NGINX_STATIC_EXPIRES:-off} volumes: - ./tools/local-env/default.template:/etc/nginx/conf.d/default.template - ./:/var/www # Load our config file, substituting environment variables into the config. - command: /bin/sh -c "envsubst '$$LOCAL_DIR $$LOCAL_NGINX_COMPRESSION' < /etc/nginx/conf.d/default.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'" + command: /bin/sh -c "envsubst '$$LOCAL_DIR $$LOCAL_NGINX_COMPRESSION $$LOCAL_NGINX_STATIC_EXPIRES' < /etc/nginx/conf.d/default.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'" depends_on: php: @@ -52,14 +53,27 @@ services: - ./tools/local-env/php-config.ini:/usr/local/etc/php/conf.d/php-config.ini - ./:/var/www - # Copy or delete the Memcached dropin plugin file as appropriate. - command: /bin/sh -c "if [ $LOCAL_PHP_MEMCACHED = true ]; then cp -n /var/www/tests/phpunit/includes/object-cache.php /var/www/src/wp-content/object-cache.php; else rm -f /var/www/src/wp-content/object-cache.php; fi && exec php-fpm" + # Install the loopback-request shim (see tools/local-env/mu-plugins/fix-docker-loopback.php), then + # copy or delete the Memcached dropin plugin file as appropriate. The shim is only installed when wp-content already + # exists, so that a not-yet-built LOCAL_DIR (e.g. build) is not created here as root, which would block the build. + command: >- + /bin/sh -c " + if [ -d /var/www/${LOCAL_DIR-src}/wp-content ]; then + mkdir -m 777 -p /var/www/${LOCAL_DIR-src}/wp-content/mu-plugins && sed -e '2a\\ * ⚠️ DO NOT EDIT THIS FILE. It is overwritten whenever the environment starts. Edit tools/local-env/mu-plugins/fix-docker-loopback.php instead.' -e '2a\\ *' /var/www/tools/local-env/mu-plugins/fix-docker-loopback.php > /var/www/${LOCAL_DIR-src}/wp-content/mu-plugins/fix-docker-loopback.php; + fi; + if [ $LOCAL_PHP_MEMCACHED = true ]; then + cp -n /var/www/tests/phpunit/includes/object-cache.php /var/www/src/wp-content/object-cache.php; + else + rm -f /var/www/src/wp-content/object-cache.php; + fi && exec php-fpm + " # The init directive ensures the command runs with a PID > 1, so Ctrl+C works correctly. init: true extra_hosts: - localhost:host-gateway + - host.docker.internal:host-gateway ## # The MySQL container. @@ -80,10 +94,13 @@ services: - ./tools/local-env/mysql-init.sql:/docker-entrypoint-initdb.d/mysql-init.sql - mysql:/var/lib/mysql + # Ping over TCP: the client resolves the literal `localhost` to the Unix socket, which the + # temporary `--skip-networking` server used to initialise a cold volume answers long before + # the real server listens on port 3306. healthcheck: test: [ 'CMD-SHELL', - 'if [ "$LOCAL_DB_TYPE" = "mariadb" ]; then case "$LOCAL_DB_VERSION" in 5.5|10.0|10.1|10.2|10.3) mysqladmin ping -h localhost || exit $$?;; *) mariadb-admin ping -h localhost || exit $$?;; esac; else mysqladmin ping -h localhost || exit $$?; fi' + 'if [ "$LOCAL_DB_TYPE" = "mariadb" ]; then case "$LOCAL_DB_VERSION" in 5.5|10.0|10.1|10.2|10.3) mysqladmin ping -h 127.0.0.1 --protocol=tcp || exit $$?;; *) mariadb-admin ping -h 127.0.0.1 --protocol=tcp || exit $$?;; esac; else mysqladmin ping -h 127.0.0.1 --protocol=tcp || exit $$?; fi' ] timeout: 5s interval: 5s @@ -118,6 +135,7 @@ services: extra_hosts: - localhost:host-gateway + - host.docker.internal:host-gateway depends_on: php: diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000000000..a3f2f384d7d7b --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,38 @@ +/** + * ESLint v10 flat config entry point. + * + * This minimal config focuses only on JSDoc linting by importing + * the configuration from .eslintrc-jsdoc.js. + * + * General JavaScript linting is not performed here. + */ + +const jsdocConfig = require( './.eslintrc-jsdoc.js' ); + +module.exports = [ + ...jsdocConfig, + { + ignores: [ + 'build/**', + '**/build/**', + 'node_modules/**', + 'tests/**', + 'vendor/**', + 'tools/**', + 'jsdoc/**', + 'artifacts/**', + 'coverage/**', + '.cache/**', + 'src/wp-includes/blocks/**/*.js', + 'src/wp-includes/blocks/**/*.js.map', + 'src/wp-content/themes/**', + 'src/wp-content/plugins/**', + 'src/wp-content/mu-plugins/**', + 'src/wp-content/upgrade/**', + 'src/wp-content/uploads/**', + 'src/js/_enqueues/vendor/**', + 'src/wp-admin/js/**', + 'src/wp-includes/js/**', + ], + }, +]; diff --git a/package-lock.json b/package-lock.json index f1d7ebd89b2c4..2eb2e25893d72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,10 +25,11 @@ "jquery-color": "3.0.0", "jquery-form": "4.3.0", "jquery-hoverintent": "1.10.2", + "jquery-migrate": "3.6.0", "jsonlint": "1.6.3", "lodash": "4.18.1", "masonry-layout": "4.2.2", - "moment": "2.30.1", + "moment": "2.31.0", "objectFitPolyfill": "2.3.5", "polyfill-library": "4.8.0", "react": "18.3.1", @@ -40,9 +41,12 @@ "wicg-inert": "3.1.3" }, "devDependencies": { + "@343dev/gifsicle": "1.2.0", "@lodder/grunt-postcss": "^3.1.1", + "@napi-rs/image": "1.14.0", "@playwright/test": "1.61.1", "@pmmmwh/react-refresh-webpack-plugin": "0.6.2", + "@types/backbone": "1.4.23", "@types/codemirror": "5.60.17", "@types/espree": "10.1.0", "@types/htmlhint": "1.1.5", @@ -54,7 +58,6 @@ "@wordpress/scripts": "33.0.0", "autoprefixer": "10.5.3", "chalk": "5.6.2", - "check-node-version": "4.2.1", "cssnano": "7.1.9", "dotenv": "17.4.2", "dotenv-expand": "13.0.0", @@ -64,7 +67,6 @@ "grunt-contrib-concat": "2.1.0", "grunt-contrib-copy": "~1.0.0", "grunt-contrib-cssmin": "~5.0.0", - "grunt-contrib-imagemin": "~4.0.0", "grunt-contrib-jshint": "3.2.0", "grunt-contrib-uglify": "~5.2.2", "grunt-contrib-watch": "~1.1.0", @@ -95,8 +97,21 @@ "webpack": "5.108.4" }, "engines": { - "node": ">=20.10.0", - "npm": ">=10.2.3" + "node": ">=24.18.0", + "npm": ">=11.16.0" + } + }, + "node_modules/@343dev/gifsicle": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@343dev/gifsicle/-/gifsicle-1.2.0.tgz", + "integrity": "sha512-sJMxT5SCdnAgf3j7GIXTYecRLPG/NYsCdPpjYhoXesVr2NqdH4HZsl6+j5M9Q5HtA/Ee4AOkTQRd98hlYHFW6A==", + "dev": true, + "license": "GPL-2.0-only", + "bin": { + "gifsicle": "cli.js" + }, + "engines": { + "node": ">=18.17" } }, "node_modules/@asamuzakjp/css-color": { @@ -4112,17 +4127,302 @@ "postcss": "^8.0.0" } }, - "node_modules/@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "node_modules/@napi-rs/image": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@napi-rs/image/-/image-1.14.0.tgz", + "integrity": "sha512-pmwJPWY6R063luNBEP9msq9lFu+kYVMiQTJRSpyGnzYnnerYpT3U4vCgFhhVR3jypCgCVWi9viDLoEJoZoIA1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/image-android-arm64": "1.14.0", + "@napi-rs/image-darwin-arm64": "1.14.0", + "@napi-rs/image-darwin-x64": "1.14.0", + "@napi-rs/image-freebsd-x64": "1.14.0", + "@napi-rs/image-linux-arm-gnueabihf": "1.14.0", + "@napi-rs/image-linux-arm64-gnu": "1.14.0", + "@napi-rs/image-linux-arm64-musl": "1.14.0", + "@napi-rs/image-linux-x64-gnu": "1.14.0", + "@napi-rs/image-linux-x64-musl": "1.14.0", + "@napi-rs/image-wasm32-wasi": "1.14.0", + "@napi-rs/image-win32-arm64-msvc": "1.14.0", + "@napi-rs/image-win32-ia32-msvc": "1.14.0", + "@napi-rs/image-win32-x64-msvc": "1.14.0" + } + }, + "node_modules/@napi-rs/image-android-arm64": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@napi-rs/image-android-arm64/-/image-android-arm64-1.14.0.tgz", + "integrity": "sha512-LBWu1fNR7uC6kNQy+axDqXKU7Kj1x3UdD3xGKw/bks6YAUG15m92qwSwINDEB4Jqgl29vCeSNFItJZRm3byfJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/image-darwin-arm64": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@napi-rs/image-darwin-arm64/-/image-darwin-arm64-1.14.0.tgz", + "integrity": "sha512-0f7SqjtXHelxbwkqdpUb/5yhVK7ciVoe902hqCeffhAFsJA7HQbEbzfM8W1MDawI6Ghenr5zHYQimWf1W1vaow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/image-darwin-x64": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@napi-rs/image-darwin-x64/-/image-darwin-x64-1.14.0.tgz", + "integrity": "sha512-wi6w06N516QzSd/QMre/Ye92mA4b5E2yQfr1+Hhml3tWrIM2Po2QvxBO9ns0AaG8u6y8b545bDzQCGLTfBykyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/image-freebsd-x64": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@napi-rs/image-freebsd-x64/-/image-freebsd-x64-1.14.0.tgz", + "integrity": "sha512-Sz1XSGF6/Xmjb33CbWLOb7h6+ekmoJRGsN0bdnzn8W86OHPTSsIbTOMtqp9ZXTLQNJgeXQhprQhhmRFNRqq0Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/image-linux-arm-gnueabihf": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@napi-rs/image-linux-arm-gnueabihf/-/image-linux-arm-gnueabihf-1.14.0.tgz", + "integrity": "sha512-ZsJ+0Rl3Ke1DN+W4lQMSzsfrwskjP4CkUsqOT7Y/HpziwirQn9R2fl2QU2Kb2eDSQvwSvGbRCkMLqH+4V4rzHg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/image-linux-arm64-gnu": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@napi-rs/image-linux-arm64-gnu/-/image-linux-arm64-gnu-1.14.0.tgz", + "integrity": "sha512-J06fGFXt76fAGEaCb3uyPQgZrwvGbpYZycA4Xz6F5dauVgcLPw75syXmggode5G+a0cSg40fjddXMvSWhPwHGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/image-linux-arm64-musl": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@napi-rs/image-linux-arm64-musl/-/image-linux-arm64-musl-1.14.0.tgz", + "integrity": "sha512-AH8WvQb58ix9/RHB+9XNF2rOB8aAnFHzj0hDCCZCplRGHmv9BzOtA+IWdf5l4f7KmEJUohY+MxVWJ1jfpgn+JQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/image-linux-x64-gnu": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@napi-rs/image-linux-x64-gnu/-/image-linux-x64-gnu-1.14.0.tgz", + "integrity": "sha512-oU3wnQcDeZf+t8vyKMP4A6okY7JNjRbiiC7j8UsJ2lDf0no3GDQEJCCFDrQ7gCKQZAI7cr0rFbI75l2KlTM0PQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/image-linux-x64-musl": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@napi-rs/image-linux-x64-musl/-/image-linux-x64-musl-1.14.0.tgz", + "integrity": "sha512-iuDTfVmFm9W6S4gzLNdSFt+Wr86LtciJJ+u2i0KBixpFYoHzeIkUr6LHBIkYg3eNdi7xHBb9qFBvxxhFQg4tUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/image-wasm32-wasi": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@napi-rs/image-wasm32-wasi/-/image-wasm32-wasi-1.14.0.tgz", + "integrity": "sha512-XRAbAQVWSm4wQzYwnTtOHD+ev6dRHUxLxmFQe5Nbev12dspqkB40zsDJ/yr4VwnV80gIfEfg7BvRLw9FFvLRlg==", + "cpu": [ + "wasm32" + ], "dev": true, + "license": "MIT", + "optional": true, "dependencies": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { - "node": ">=4" + "node": ">=14.0.0" + } + }, + "node_modules/@napi-rs/image-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/image-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/image-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/image-win32-arm64-msvc": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@napi-rs/image-win32-arm64-msvc/-/image-win32-arm64-msvc-1.14.0.tgz", + "integrity": "sha512-Wuq0AfN5rjrv2SkJTubIIYW//B5aBloPxND9GxRHKgGkLhyiSxBD6iOCQA1M/n12rvF9BWeadDDO8JLoO3iRHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/image-win32-ia32-msvc": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@napi-rs/image-win32-ia32-msvc/-/image-win32-ia32-msvc-1.14.0.tgz", + "integrity": "sha512-Idb+OyISq24YRdO8iU+BC6QaN4pU6HpC+SRrxFOftWJg0xXPzfPfpwpwdviduJK/SjJcHe02nhUq3HKHtUEnWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/image-win32-x64-msvc": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@napi-rs/image-win32-x64-msvc/-/image-win32-x64-msvc-1.14.0.tgz", + "integrity": "sha512-Hq5BY3+FeNehRe1VPhVQ28dGTSWlxnGAHeoF7/GpgeWBfltLT5+DpkV+FPyq6ju49kG1aFofE+IlSGi5sMJ0Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" } }, "node_modules/@napi-rs/wasm-runtime": { @@ -4819,16 +5119,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@sindresorhus/is": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", - "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", - "dev": true, - "optional": true, - "engines": { - "node": ">=4" - } - }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -5586,6 +5876,17 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/backbone": { + "version": "1.4.23", + "resolved": "https://registry.npmjs.org/@types/backbone/-/backbone-1.4.23.tgz", + "integrity": "sha512-B/hN/DAJdWFOusEkEoa5xgfVuxJJPOR/6JQ2uwURPDyKL24PuC76IR6EcSRJ6lvGWtxHRQYMJQhJYm6KpMDtGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jquery": "*", + "@types/underscore": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -5868,13 +6169,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/q": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz", - "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==", - "dev": true, - "optional": true - }, "node_modules/@types/qs": { "version": "6.9.11", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", @@ -8758,59 +9052,14 @@ "node": ">= 8" } }, - "node_modules/arch": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", - "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" - } - ], - "optional": true - }, - "node_modules/archive-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", - "integrity": "sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=", + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", "dev": true, - "optional": true, - "dependencies": { - "file-type": "^4.2.0" - }, + "license": "MIT", "engines": { - "node": ">=4" - } - }, - "node_modules/archive-type/node_modules/file-type": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", - "integrity": "sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/are-docs-informative": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", - "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" + "node": ">=14" } }, "node_modules/argparse": { @@ -8839,33 +9088,6 @@ "node": ">= 0.4" } }, - "node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -8891,16 +9113,6 @@ "node": ">=0.10.0" } }, - "node_modules/array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -8939,37 +9151,6 @@ "node": ">=0.10.0" } }, - "node_modules/array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-uniq": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -9116,15 +9297,6 @@ "node": ">=0.8" } }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", @@ -9178,18 +9350,6 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true, - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, "node_modules/autoprefixer": { "version": "10.5.3", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.3.tgz", @@ -9740,51 +9900,6 @@ "bare-path": "^3.0.0" } }, - "node_modules/base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -9852,486 +9967,145 @@ "node": "*" } }, - "node_modules/bin-build": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bin-build/-/bin-build-3.0.0.tgz", - "integrity": "sha512-jcUOof71/TNAI2uM5uoUaDq2ePcVBQ3R/qhxAz1rX7UfvduAL/RXD3jXzvn8cVcDJdGVkiR1shal3OH0ImpuhA==", + "node_modules/binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true, - "optional": true, - "dependencies": { - "decompress": "^4.0.0", - "download": "^6.2.2", - "execa": "^0.7.0", - "p-map-series": "^1.0.0", - "tempfile": "^2.0.0" - }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/bin-build/node_modules/execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/body": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", + "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=4" + "continuable-cache": "^0.3.1", + "error": "^7.0.0", + "raw-body": "~1.1.0", + "safe-json-parse": "~1.0.1" } }, - "node_modules/bin-build/node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "dev": true, "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/bin-check": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", - "integrity": "sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==", - "dev": true, - "optional": true, "dependencies": { - "execa": "^0.7.0", - "executable": "^4.1.0" + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" }, "engines": { - "node": ">=4" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/bin-check/node_modules/execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=4" + "ms": "2.0.0" } }, - "node_modules/bin-check/node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/bin-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-3.1.0.tgz", - "integrity": "sha512-Mkfm4iE1VFt4xd4vH+gx+0/71esbfus2LsnCGe8Pi4mndSPyT+NGES/Eg99jx8/lUGWfu3z2yuB/bt5UB+iVbQ==", - "dev": true, - "optional": true, "dependencies": { - "execa": "^1.0.0", - "find-versions": "^3.0.0" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": ">=6" + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/bin-version-check": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-4.0.0.tgz", - "integrity": "sha512-sR631OrhC+1f8Cvs8WyVWOA33Y8tgwjETNPyyD/myRBXLkfS/vl74FmH/lFcRl9KY3zwGh7jFhvyk9vV3/3ilQ==", + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, - "optional": true, + "license": "MIT", "dependencies": { - "bin-version": "^3.0.0", - "semver": "^5.6.0", - "semver-truncate": "^1.1.2" + "safer-buffer": ">= 2.1.2 < 3" }, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/bin-version-check/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, - "optional": true, - "bin": { - "semver": "bin/semver" - } + "license": "MIT" }, - "node_modules/bin-wrapper": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bin-wrapper/-/bin-wrapper-4.1.0.tgz", - "integrity": "sha512-hfRmo7hWIXPkbpi0ZltboCMVrU+0ClXR/JgbCKKjlDjQf6igXa7OwdqNcFWQZPZTgiY7ZpzE3+LjjkLiTN2T7Q==", + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, - "optional": true, - "dependencies": { - "bin-check": "^4.1.0", - "bin-version-check": "^4.0.0", - "download": "^7.1.0", - "import-lazy": "^3.1.0", - "os-filter-obj": "^2.0.0", - "pify": "^4.0.1" - }, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.8" } }, - "node_modules/bin-wrapper/node_modules/download": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/download/-/download-7.1.0.tgz", - "integrity": "sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==", + "node_modules/body/node_modules/bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", + "integrity": "sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==", + "dev": true + }, + "node_modules/body/node_modules/raw-body": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", + "integrity": "sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg==", "dev": true, - "optional": true, + "license": "MIT", "dependencies": { - "archive-type": "^4.0.0", - "caw": "^2.0.1", - "content-disposition": "^0.5.2", - "decompress": "^4.2.0", - "ext-name": "^5.0.0", - "file-type": "^8.1.0", - "filenamify": "^2.0.0", - "get-stream": "^3.0.0", - "got": "^8.3.1", - "make-dir": "^1.2.0", - "p-event": "^2.1.0", - "pify": "^3.0.0" + "bytes": "1", + "string_decoder": "0.10" }, "engines": { - "node": ">=6" + "node": ">= 0.8.0" } }, - "node_modules/bin-wrapper/node_modules/download/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "node_modules/body/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/bin-wrapper/node_modules/file-type": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz", - "integrity": "sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==", - "dev": true, - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/bin-wrapper/node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/bin-wrapper/node_modules/got": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", - "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", - "dev": true, - "optional": true, - "dependencies": { - "@sindresorhus/is": "^0.7.0", - "cacheable-request": "^2.1.1", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "into-stream": "^3.1.0", - "is-retry-allowed": "^1.1.0", - "isurl": "^1.0.0-alpha5", - "lowercase-keys": "^1.0.0", - "mimic-response": "^1.0.0", - "p-cancelable": "^0.4.0", - "p-timeout": "^2.0.1", - "pify": "^3.0.0", - "safe-buffer": "^5.1.1", - "timed-out": "^4.0.1", - "url-parse-lax": "^3.0.0", - "url-to-options": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/bin-wrapper/node_modules/got/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/bin-wrapper/node_modules/import-lazy": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz", - "integrity": "sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ==", - "dev": true, - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/bin-wrapper/node_modules/make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "optional": true, - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/bin-wrapper/node_modules/make-dir/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/bin-wrapper/node_modules/p-cancelable": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", - "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", - "dev": true, - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/bin-wrapper/node_modules/p-event": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", - "integrity": "sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==", - "dev": true, - "optional": true, - "dependencies": { - "p-timeout": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/bin-wrapper/node_modules/p-timeout": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", - "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", - "dev": true, - "optional": true, - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/bin-wrapper/node_modules/prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/bin-wrapper/node_modules/url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "prepend-http": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "node_modules/body": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", - "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=", - "dev": true, - "dependencies": { - "continuable-cache": "^0.3.1", - "error": "^7.0.0", - "raw-body": "~1.1.0", - "safe-json-parse": "~1.0.1" - } - }, - "node_modules/body-parser": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", - "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/body-parser/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body/node_modules/bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", - "integrity": "sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==", - "dev": true - }, - "node_modules/body/node_modules/raw-body": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", - "integrity": "sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "1", - "string_decoder": "0.10" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/body/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "dev": true, - "license": "MIT" + "license": "MIT" }, "node_modules/bonjour-service": { "version": "1.2.1", @@ -10360,40 +10134,6 @@ "concat-map": "0.0.1" } }, - "node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/browserslist": { "version": "4.28.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", @@ -10461,24 +10201,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "dev": true, - "optional": true, - "dependencies": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "node_modules/buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "dev": true, - "optional": true - }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -10488,13 +10210,6 @@ "node": "*" } }, - "node_modules/buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", - "dev": true, - "optional": true - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -10520,26 +10235,6 @@ "node": ">= 0.8" } }, - "node_modules/cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "dependencies": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cacheable": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", @@ -10554,88 +10249,6 @@ "qified": "^0.10.1" } }, - "node_modules/cacheable-request": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", - "integrity": "sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==", - "dev": true, - "optional": true, - "dependencies": { - "clone-response": "1.0.2", - "get-stream": "3.0.0", - "http-cache-semantics": "3.8.1", - "keyv": "3.0.0", - "lowercase-keys": "1.0.0", - "normalize-url": "2.0.1", - "responselike": "1.0.2" - } - }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/cacheable-request/node_modules/normalize-url": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", - "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", - "dev": true, - "optional": true, - "dependencies": { - "prepend-http": "^2.0.0", - "query-string": "^5.0.1", - "sort-keys": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cacheable-request/node_modules/prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/cacheable-request/node_modules/query-string": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", - "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", - "dev": true, - "optional": true, - "dependencies": { - "decode-uri-component": "^0.2.0", - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cacheable-request/node_modules/sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-plain-obj": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/cacheable/node_modules/keyv": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", @@ -10696,12 +10309,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", - "dev": true - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -10730,32 +10337,6 @@ "node": ">=6" } }, - "node_modules/camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/camelcase-keys/node_modules/camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -10818,22 +10399,6 @@ "node": ">= 10" } }, - "node_modules/caw": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", - "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", - "dev": true, - "optional": true, - "dependencies": { - "get-proxy": "^2.0.0", - "isurl": "^1.0.0-alpha5", - "tunnel-agent": "^0.6.0", - "url-to-options": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -11109,34 +10674,6 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, - "node_modules/class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "dependencies": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/clean-css": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", @@ -11268,16 +10805,6 @@ "node": ">=0.8" } }, - "node_modules/clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "dev": true, - "optional": true, - "dependencies": { - "mimic-response": "^1.0.0" - } - }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -11288,36 +10815,6 @@ "node": ">= 0.12.0" } }, - "node_modules/coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", - "dev": true, - "optional": true, - "dependencies": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/coa/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "optional": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/codemirror": { "version": "5.65.20", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.20.tgz", @@ -11344,19 +10841,6 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, - "node_modules/collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "dependencies": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -11439,12 +10923,6 @@ "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", "dev": true }, - "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -11507,17 +10985,6 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "node_modules/config-chain": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", - "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", - "dev": true, - "optional": true, - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, "node_modules/configstore": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", @@ -11556,13 +11023,6 @@ "node": ">=0.8" } }, - "node_modules/console-stream": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/console-stream/-/console-stream-0.1.1.tgz", - "integrity": "sha1-oJX+B7IEZZVfL6/Si11yvM2UnUQ=", - "dev": true, - "optional": true - }, "node_modules/constant-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", @@ -11624,15 +11084,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true }, - "node_modules/copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/copy-webpack-plugin": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz", @@ -11927,52 +11378,7 @@ }, "engines": { "node": ">=8" - } - }, - "node_modules/cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "node_modules/cross-spawn/node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "optional": true, - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "optional": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/cross-spawn/node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", - "dev": true, - "license": "ISC", - "optional": true + } }, "node_modules/crypto-random-string": { "version": "2.0.0", @@ -12046,63 +11452,6 @@ "webpack": "^5.0.0" } }, - "node_modules/css-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", - "dev": true, - "optional": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "node_modules/css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", - "dev": true, - "optional": true - }, - "node_modules/css-tree": { - "version": "1.0.0-alpha.37", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", - "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", - "dev": true, - "optional": true, - "dependencies": { - "mdn-data": "2.0.4", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/css-tree/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", - "dev": true, - "optional": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -12210,51 +11559,6 @@ "postcss": "^8.5.13" } }, - "node_modules/csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "dev": true, - "optional": true, - "dependencies": { - "css-tree": "^1.1.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true, - "optional": true - }, - "node_modules/csso/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cssstyle": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", @@ -12276,19 +11580,6 @@ "dev": true, "license": "MIT" }, - "node_modules/currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "optional": true, - "dependencies": { - "array-find-index": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -12371,326 +11662,87 @@ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", - "dev": true - }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", - "dev": true - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dev": true, - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/decompress": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", - "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", - "dev": true, - "optional": true, - "dependencies": { - "decompress-tar": "^4.0.0", - "decompress-tarbz2": "^4.0.0", - "decompress-targz": "^4.0.0", - "decompress-unzip": "^4.0.1", - "graceful-fs": "^4.1.10", - "make-dir": "^1.0.0", - "pify": "^2.3.0", - "strip-dirs": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "dev": true, - "optional": true, - "dependencies": { - "mimic-response": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-tar": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", - "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", - "dev": true, - "optional": true, - "dependencies": { - "file-type": "^5.2.0", - "is-stream": "^1.1.0", - "tar-stream": "^1.5.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-tar/node_modules/bl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", - "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", - "dev": true, - "optional": true, - "dependencies": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/decompress-tar/node_modules/file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-tar/node_modules/tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", - "dev": true, - "optional": true, - "dependencies": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/decompress-tarbz2": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", - "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", - "dev": true, - "optional": true, - "dependencies": { - "decompress-tar": "^4.1.0", - "file-type": "^6.1.0", - "is-stream": "^1.1.0", - "seek-bzip": "^1.0.5", - "unbzip2-stream": "^1.0.9" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-tarbz2/node_modules/file-type": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", - "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-targz": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", - "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", - "dev": true, - "optional": true, - "dependencies": { - "decompress-tar": "^4.1.1", - "file-type": "^5.2.0", - "is-stream": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-targz/node_modules/file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-unzip": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", - "dev": true, - "optional": true, - "dependencies": { - "file-type": "^3.8.0", - "get-stream": "^2.2.0", - "pify": "^2.3.0", - "yauzl": "^2.4.2" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/decompress-unzip/node_modules/file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "node_modules/date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", "dev": true, - "license": "MIT", - "optional": true, "engines": { - "node": ">=0.10.0" + "node": "*" } }, - "node_modules/decompress-unzip/node_modules/get-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", - "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "object-assign": "^4.0.1", - "pinkie-promise": "^2.0.0" + "ms": "^2.1.3" }, "engines": { - "node": ">=0.10.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/decompress-unzip/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true, - "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/decompress/node_modules/make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", "dev": true, - "optional": true, "dependencies": { - "pify": "^3.0.0" + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/decompress/node_modules/make-dir/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/decompress/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, "node_modules/dedent": { "version": "1.5.1", @@ -12899,33 +11951,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-property/node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/degenerator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", @@ -13136,17 +12161,6 @@ } ] }, - "node_modules/domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, - "optional": true, - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", @@ -13198,75 +12212,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/download": { - "version": "6.2.5", - "resolved": "https://registry.npmjs.org/download/-/download-6.2.5.tgz", - "integrity": "sha512-DpO9K1sXAST8Cpzb7kmEhogJxymyVUd5qz/vCOSyvwtp2Klj2XcDt5YUuasgxka44SxF0q5RriKIwJmQHG2AuA==", - "dev": true, - "optional": true, - "dependencies": { - "caw": "^2.0.0", - "content-disposition": "^0.5.2", - "decompress": "^4.0.0", - "ext-name": "^5.0.0", - "file-type": "5.2.0", - "filenamify": "^2.0.0", - "get-stream": "^3.0.0", - "got": "^7.0.0", - "make-dir": "^1.0.0", - "p-event": "^1.0.0", - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/download/node_modules/file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/download/node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/download/node_modules/make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "optional": true, - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/download/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -13287,13 +12232,6 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, - "node_modules/duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true, - "optional": true - }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -14620,155 +13558,12 @@ }, "node_modules/events-universal": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, - "node_modules/exec-buffer": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/exec-buffer/-/exec-buffer-3.2.0.tgz", - "integrity": "sha512-wsiD+2Tp6BWHoVv3B+5Dcx6E7u5zky+hUwOHjuH2hKSLR3dvRmX8fk8UD8uqQixHs4Wk6eDmiegVrMPjKj7wpA==", - "dev": true, - "optional": true, - "dependencies": { - "execa": "^0.7.0", - "p-finally": "^1.0.0", - "pify": "^3.0.0", - "rimraf": "^2.5.4", - "tempfile": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/exec-buffer/node_modules/execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/exec-buffer/node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/exec-buffer/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "optional": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/execa/node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/execa/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "optional": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/execa/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "optional": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/executable": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", - "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", - "dev": true, - "optional": true, - "dependencies": { - "pify": "^2.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/executable/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" } }, "node_modules/exit": { @@ -14790,66 +13585,6 @@ "node": ">= 0.8.0" } }, - "node_modules/expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "dependencies": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/expand-brackets/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", @@ -14935,77 +13670,12 @@ "dev": true, "license": "MIT" }, - "node_modules/ext-list": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", - "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", - "dev": true, - "optional": true, - "dependencies": { - "mime-db": "^1.28.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ext-name": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", - "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", - "dev": true, - "optional": true, - "dependencies": { - "ext-list": "^2.0.0", - "sort-keys-length": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, - "node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extend-shallow/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extend-shallow/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/external-editor": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", @@ -15032,65 +13702,6 @@ "node": ">=0.10.0" } }, - "node_modules/extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -15259,26 +13870,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/fast-xml-parser": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.7.tgz", - "integrity": "sha512-a6Qh1RMCNbSrU1+sAyAAZH3rTe+OaWJbNZIq0S+ifZciUUOQtlVxBJwoTUE2bYhysmG/RYyI5WJFIKdBahJdrQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -15368,21 +13959,6 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -15402,69 +13978,6 @@ "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=", "dev": true }, - "node_modules/file-type": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz", - "integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/filename-reserved-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", - "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", - "dev": true, - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/filenamify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", - "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", - "dev": true, - "optional": true, - "dependencies": { - "filename-reserved-regex": "^2.0.0", - "strip-outer": "^1.0.0", - "trim-repeated": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -15639,19 +14152,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-versions": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", - "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", - "dev": true, - "optional": true, - "dependencies": { - "semver-regex": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/findup-sync": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", @@ -16002,18 +14502,6 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "dependencies": { - "map-cache": "^0.2.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -16041,13 +14529,6 @@ "from2": "^2.0.3" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "optional": true - }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -16215,48 +14696,11 @@ "node": ">= 0.4" } }, - "node_modules/get-proxy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", - "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", - "dev": true, - "optional": true, - "dependencies": { - "npm-conf": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/get-size": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/get-size/-/get-size-2.0.3.tgz", "integrity": "sha512-lXNzT/h/dTjTxRbm9BXb+SGxxzkm97h/PCIKtlN/CBCxxmkkIVV21udumMS93MuVTDX583gqc94v3RjuHmI+2Q==" }, - "node_modules/get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "optional": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -16302,15 +14746,6 @@ "node": ">= 14" } }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/getobject": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.0.tgz", @@ -16337,27 +14772,7 @@ "license": "MIT", "dependencies": { "encoding": "^0.1.12", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/gifsicle": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/gifsicle/-/gifsicle-4.0.1.tgz", - "integrity": "sha512-A/kiCLfDdV+ERV/UB+2O41mifd+RxH8jlRG8DMxZO84Bma/Fw0htqZ+hY2iaalLRNyUu7tYZQslqUBJxBggxbg==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "dependencies": { - "bin-build": "^3.0.0", - "bin-wrapper": "^4.0.0", - "execa": "^1.0.0", - "logalot": "^2.0.0" - }, - "bin": { - "gifsicle": "cli.js" - }, - "engines": { - "node": ">=6" + "safe-buffer": "^5.1.1" } }, "node_modules/glob": { @@ -16394,13 +14809,6 @@ "node": ">= 6" } }, - "node_modules/glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==", - "dev": true, - "license": "BSD" - }, "node_modules/global-modules": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", @@ -16572,43 +14980,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz", - "integrity": "sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==", - "dev": true, - "optional": true, - "dependencies": { - "decompress-response": "^3.2.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-plain-obj": "^1.1.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "isurl": "^1.0.0-alpha5", - "lowercase-keys": "^1.0.0", - "p-cancelable": "^0.3.0", - "p-timeout": "^1.1.1", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "url-parse-lax": "^1.0.0", - "url-to-options": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/got/node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -16983,72 +15354,6 @@ "node": ">=8" } }, - "node_modules/grunt-contrib-imagemin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-imagemin/-/grunt-contrib-imagemin-4.0.0.tgz", - "integrity": "sha512-2GYQBQFfJLjeTThJ8E7+vLgvgfOh78u0bgieIK85c2Rv9V6ssd2AvBvuF7T26mK261EN/SlNefpW5+zGWzfrVw==", - "dev": true, - "dependencies": { - "chalk": "^2.4.1", - "imagemin": "^6.0.0", - "p-map": "^1.2.0", - "plur": "^3.0.1", - "pretty-bytes": "^5.1.0" - }, - "engines": { - "node": ">=8" - }, - "optionalDependencies": { - "imagemin-gifsicle": "^6.0.1", - "imagemin-jpegtran": "^6.0.0", - "imagemin-optipng": "^6.0.0", - "imagemin-svgo": "^7.0.0" - } - }, - "node_modules/grunt-contrib-imagemin/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/grunt-contrib-imagemin/node_modules/irregular-plurals": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-2.0.0.tgz", - "integrity": "sha512-Y75zBYLkh0lJ9qxeHlMjQ7bSbyiSqNW/UOPWDmzC7cXskL1hekSITh1Oc6JV0XCWWZ9DE8VYSB71xocLk3gmGw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/grunt-contrib-imagemin/node_modules/p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/grunt-contrib-imagemin/node_modules/plur": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/plur/-/plur-3.1.1.tgz", - "integrity": "sha512-t1Ax8KUvV3FFII8ltczPn2tJdjqbd1sIzu6t4JL7nQ3EyeL/lTrj5PWKb06ic5/6XYDr65rQ4uzQEGN70/6X5w==", - "dev": true, - "dependencies": { - "irregular-plurals": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/grunt-contrib-jshint": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/grunt-contrib-jshint/-/grunt-contrib-jshint-3.2.0.tgz", @@ -17884,16 +16189,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbol-support-x": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", - "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", - "dev": true, - "optional": true, - "engines": { - "node": "*" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -17906,19 +16201,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-to-string-tag-x": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", - "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", - "dev": true, - "optional": true, - "dependencies": { - "has-symbol-support-x": "^1.4.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/has-tostringtag": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", @@ -17934,48 +16216,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/hashery": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", @@ -18349,13 +16589,6 @@ "dev": true, "license": "MIT" }, - "node_modules/http-cache-semantics": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", - "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", - "dev": true, - "optional": true - }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -18618,244 +16851,15 @@ "minimatch": "^3.0.4" }, "engines": { - "node": ">=10" - } - }, - "node_modules/image-ssim": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz", - "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==", - "dev": true - }, - "node_modules/imagemin": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/imagemin/-/imagemin-6.1.0.tgz", - "integrity": "sha512-8ryJBL1CN5uSHpiBMX0rJw79C9F9aJqMnjGnrd/1CafegpNuA81RBAAru/jQQEOWlOJJlpRnlcVFF6wq+Ist0A==", - "dev": true, - "dependencies": { - "file-type": "^10.7.0", - "globby": "^8.0.1", - "make-dir": "^1.0.0", - "p-pipe": "^1.1.0", - "pify": "^4.0.1", - "replace-ext": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/imagemin-gifsicle": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/imagemin-gifsicle/-/imagemin-gifsicle-6.0.1.tgz", - "integrity": "sha512-kuu47c6iKDQ6R9J10xCwL0lgs0+sMz3LRHqRcJ2CRBWdcNmo3T5hUaM8hSZfksptZXJLGKk8heSAvwtSdB1Fng==", - "dev": true, - "optional": true, - "dependencies": { - "exec-buffer": "^3.0.0", - "gifsicle": "^4.0.0", - "is-gif": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/imagemin-jpegtran": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/imagemin-jpegtran/-/imagemin-jpegtran-6.0.0.tgz", - "integrity": "sha512-Ih+NgThzqYfEWv9t58EItncaaXIHR0u9RuhKa8CtVBlMBvY0dCIxgQJQCfwImA4AV1PMfmUKlkyIHJjb7V4z1g==", - "dev": true, - "optional": true, - "dependencies": { - "exec-buffer": "^3.0.0", - "is-jpg": "^2.0.0", - "jpegtran-bin": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/imagemin-optipng": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/imagemin-optipng/-/imagemin-optipng-6.0.0.tgz", - "integrity": "sha512-FoD2sMXvmoNm/zKPOWdhKpWdFdF9qiJmKC17MxZJPH42VMAp17/QENI/lIuP7LCUnLVAloO3AUoTSNzfhpyd8A==", - "dev": true, - "optional": true, - "dependencies": { - "exec-buffer": "^3.0.0", - "is-png": "^1.0.0", - "optipng-bin": "^5.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/imagemin-svgo": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/imagemin-svgo/-/imagemin-svgo-7.1.0.tgz", - "integrity": "sha512-0JlIZNWP0Luasn1HT82uB9nU9aa+vUj6kpT+MjPW11LbprXC+iC4HDwn1r4Q2/91qj4iy9tRZNsFySMlEpLdpg==", - "dev": true, - "optional": true, - "dependencies": { - "is-svg": "^4.2.1", - "svgo": "^1.3.2" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sindresorhus/imagemin-svgo?sponsor=1" - } - }, - "node_modules/imagemin/node_modules/@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/imagemin/node_modules/dir-glob": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", - "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", - "dev": true, - "dependencies": { - "arrify": "^1.0.1", - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/imagemin/node_modules/fast-glob": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", - "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", - "dev": true, - "dependencies": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/imagemin/node_modules/glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", - "dev": true, - "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "node_modules/imagemin/node_modules/glob-parent/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/imagemin/node_modules/globby": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz", - "integrity": "sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==", - "dev": true, - "dependencies": { - "array-union": "^1.0.1", - "dir-glob": "2.0.0", - "fast-glob": "^2.0.2", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/imagemin/node_modules/globby/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/imagemin/node_modules/ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "node_modules/imagemin/node_modules/make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/imagemin/node_modules/make-dir/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/imagemin/node_modules/path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/imagemin/node_modules/path-type/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/imagemin/node_modules/slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, + "node_modules/image-ssim": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz", + "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==", + "dev": true + }, "node_modules/imagesloaded": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/imagesloaded/-/imagesloaded-5.0.0.tgz", @@ -18926,20 +16930,6 @@ "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha512-aqwDFWSgSgfRaEwao5lg5KEcVd/2a+D1rvoG7NdilmYz0NwRk6StWpWdz/Hpk34MKPpx7s8XxUqimfcQK6gGlg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "repeating": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -19087,20 +17077,6 @@ "tslib": "2" } }, - "node_modules/into-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", - "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", - "dev": true, - "optional": true, - "dependencies": { - "from2": "^2.1.1", - "p-is-promise": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/ip-address": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", @@ -19143,19 +17119,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-accessor-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", - "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -19241,12 +17204,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, "node_modules/is-bun-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", @@ -19285,19 +17242,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-data-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", - "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/is-data-view": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", @@ -19331,20 +17275,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -19360,16 +17290,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -19393,19 +17313,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -19443,19 +17350,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-gif": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-gif/-/is-gif-3.0.0.tgz", - "integrity": "sha512-IqJ/jlbw5WJSNfwQ/lHEDXF8rxhRgF6ythk2oiEvhpG29F704eX9NO6TvPfMiq9DrbwgcEDnETYNcZDPewQoVw==", - "dev": true, - "optional": true, - "dependencies": { - "file-type": "^10.4.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -19467,16 +17361,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-jpg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-jpg/-/is-jpg-2.0.0.tgz", - "integrity": "sha1-LhmX+m6RZuqsAkLarkQ0A+TvHZc=", - "dev": true, - "optional": true, - "engines": { - "node": ">=6" - } - }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -19489,13 +17373,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-natural-number": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", - "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", - "dev": true, - "optional": true - }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -19509,19 +17386,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", @@ -19547,16 +17411,6 @@ "node": ">=8" } }, - "node_modules/is-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", - "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", - "dev": true, - "optional": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", @@ -19576,16 +17430,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-png": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-png/-/is-png-1.1.0.tgz", - "integrity": "sha1-1XSxK/J1wDUEVVcLDltXqwYgd84=", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -19623,16 +17467,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-retry-allowed": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", - "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-set": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", @@ -19660,17 +17494,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -19687,22 +17510,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-svg": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-4.4.0.tgz", - "integrity": "sha512-v+AgVwiK5DsGtT9ng+m4mClp6zDAmwrW8nZi6Gg15qzvBnRWWdfWA1TGaXyCDnWq5g5asofIgMVl3PjKxvk1ug==", - "dev": true, - "optional": true, - "dependencies": { - "fast-xml-parser": "^4.1.3" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-symbol": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", @@ -19765,13 +17572,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true, - "optional": true - }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -19985,20 +17785,6 @@ "node": ">=8" } }, - "node_modules/isurl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", - "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", - "dev": true, - "optional": true, - "dependencies": { - "has-to-string-tag-x": "^1.2.0", - "is-object": "^1.0.1" - }, - "engines": { - "node": ">= 4" - } - }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -22273,25 +20059,6 @@ "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", "dev": true }, - "node_modules/jpegtran-bin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jpegtran-bin/-/jpegtran-bin-4.0.0.tgz", - "integrity": "sha512-2cRl1ism+wJUoYAYFt6O/rLBfpXNWG2dUWbgcEkTt5WGMnqI46eEro8T4C5zGROxKRqyKpCBSdHPvt5UYCtxaQ==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "dependencies": { - "bin-build": "^3.0.0", - "bin-wrapper": "^4.0.0", - "logalot": "^2.0.0" - }, - "bin": { - "jpegtran": "cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/jquery": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", @@ -22324,6 +20091,15 @@ "jquery": ">=1.7.0" } }, + "node_modules/jquery-migrate": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery-migrate/-/jquery-migrate-3.6.0.tgz", + "integrity": "sha512-2k3GMQXXEellCOkj8tczgCd/Y0KMm7LErz4rE6CkHIZV1JTUDlgs7XFN+GpwGKXDQtw/HsjtJuE+CS8IfEOnPg==", + "license": "MIT", + "peerDependencies": { + "jquery": ">=3 <4" + } + }, "node_modules/js-library-detector": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.7.0.tgz", @@ -22591,14 +20367,6 @@ "node": ">=0.8.0" } }, - "node_modules/json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -22724,29 +20492,6 @@ "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", "dev": true }, - "node_modules/keyv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", - "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", - "dev": true, - "optional": true, - "dependencies": { - "json-buffer": "3.0.0" - } - }, - "node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/klaw": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", @@ -23137,62 +20882,6 @@ "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==", "dev": true }, - "node_modules/load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "optional": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "error-ex": "^1.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/load-json-file/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/load-json-file/node_modules/strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-utf8": "^0.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/loader-runner": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", @@ -23367,43 +21056,19 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": ">=8" - } - }, - "node_modules/log-symbols/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/logalot": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/logalot/-/logalot-2.1.0.tgz", - "integrity": "sha1-X46MkNME7fElMJUaVVSruMXj9VI=", - "dev": true, - "optional": true, - "dependencies": { - "figures": "^1.3.5", - "squeak": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "optional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/lookup-closest-locale": { @@ -23423,20 +21088,6 @@ "loose-envify": "cli.js" } }, - "node_modules/loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "optional": true, - "dependencies": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -23446,36 +21097,6 @@ "tslib": "^2.0.3" } }, - "node_modules/lowercase-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", - "integrity": "sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lpad-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/lpad-align/-/lpad-align-1.1.2.tgz", - "integrity": "sha1-IfYArBwwlcPG5JfuZyce4ISB/p4=", - "dev": true, - "optional": true, - "dependencies": { - "get-stdin": "^4.0.1", - "indent-string": "^2.1.0", - "longest": "^1.0.0", - "meow": "^3.3.0" - }, - "bin": { - "lpad-align": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -23567,18 +21188,6 @@ "integrity": "sha1-douOecAJvytk/ugG4ip7HEGQyZA=", "dev": true }, - "node_modules/map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "dependencies": { - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/markdown-it": { "version": "12.3.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", @@ -23889,13 +21498,6 @@ "node": ">=8" } }, - "node_modules/mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", - "dev": true, - "optional": true - }, "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", @@ -23931,29 +21533,6 @@ "dev": true, "license": "MIT" }, - "node_modules/meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -23993,39 +21572,6 @@ "node": ">= 0.6" } }, - "node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/micromatch/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -24069,16 +21615,6 @@ "node": ">=6" } }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "optional": true, - "engines": { - "node": ">=4" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -24347,43 +21883,6 @@ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "dev": true }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mixin-deep/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mixin-deep/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -24406,9 +21905,10 @@ } }, "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.31.0.tgz", + "integrity": "sha512-0acOTfMiWOheYS4eoWb80yYMb/JLvVv9SHbs2PehaDzfUG0Bw855SKyk0IKTnPGa5+U2bmi3W68l1+sGLX/pvw==", + "license": "MIT", "engines": { "node": "*" } @@ -24473,37 +21973,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -24564,13 +22033,6 @@ "node": ">= 0.10.0" } }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true, - "optional": true - }, "node_modules/nise": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", @@ -24835,31 +22297,6 @@ "npm-normalize-package-bin": "^1.0.1" } }, - "node_modules/npm-conf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", - "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", - "dev": true, - "optional": true, - "dependencies": { - "config-chain": "^1.1.11", - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-conf/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, "node_modules/npm-normalize-package-bin": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", @@ -25281,30 +22718,6 @@ "node": ">=10" } }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dev": true, - "optional": true, - "dependencies": { - "boolbase": "~1.0.0" - } - }, "node_modules/nwsapi": { "version": "2.2.23", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", @@ -25330,33 +22743,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-filter": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/object-filter/-/object-filter-1.0.2.tgz", @@ -25385,18 +22771,6 @@ "node": ">= 0.4" } }, - "node_modules/object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "dependencies": { - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object.assign": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", @@ -25480,24 +22854,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.getownpropertydescriptors": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz", - "integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==", - "dev": true, - "optional": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.groupby": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", @@ -25676,38 +23032,6 @@ "node": ">= 0.8.0" } }, - "node_modules/optipng-bin": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/optipng-bin/-/optipng-bin-5.1.0.tgz", - "integrity": "sha512-9baoqZTNNmXQjq/PQTWEXbVV3AMO2sI/GaaqZJZ8SExfAzjijeAP7FEeT+TtyumSw7gr0PZtSUYB/Ke7iHQVKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "dependencies": { - "bin-build": "^3.0.0", - "bin-wrapper": "^4.0.0", - "logalot": "^2.0.0" - }, - "bin": { - "optipng": "cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/os-filter-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", - "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==", - "dev": true, - "optional": true, - "dependencies": { - "arch": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -25760,51 +23084,7 @@ "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-cancelable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz", - "integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==", - "dev": true, - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-event": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-1.3.0.tgz", - "integrity": "sha512-hV1zbA7gwqPVFcapfeATaNjQ3J0NuzorHPyG8GPL9g/Y/TplWVBVoCKCXL6Ej2zscrCEv195QNWJXuBH6XZuzA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "p-timeout": "^1.1.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true, - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-is-promise": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", - "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", - "dev": true, - "optional": true, - "engines": { - "node": ">=4" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/p-limit": { @@ -25854,38 +23134,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map-series": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz", - "integrity": "sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco=", - "dev": true, - "optional": true, - "dependencies": { - "p-reduce": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-pipe": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz", - "integrity": "sha1-SxoROZoRUgpneQ7loMHViB1r7+k=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-reduce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", - "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=", - "dev": true, - "optional": true, - "engines": { - "node": ">=4" - } - }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -25899,20 +23147,6 @@ "node": ">=8" } }, - "node_modules/p-timeout": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", - "integrity": "sha512-gb0ryzr+K2qFqFv6qi3khoeqMZF/+ajxQipEF6NteZVnvz9tzdsfAVj3lYtn1gAXvH5lfLwfxEII799gt/mRIA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -26091,15 +23325,6 @@ "tslib": "^2.0.3" } }, - "node_modules/pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", @@ -26110,12 +23335,6 @@ "tslib": "^2.0.3" } }, - "node_modules/path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -26135,17 +23354,6 @@ "node": ">=0.10.0" } }, - "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -26272,29 +23480,6 @@ "node": ">=6" } }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "optional": true, - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -26418,15 +23603,6 @@ "node": ">=12" } }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -27373,17 +24549,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/prettier": { "name": "wp-prettier", "version": "3.0.3", @@ -27503,13 +24668,6 @@ "dev": true, "license": "MIT" }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", - "dev": true, - "optional": true - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -27568,13 +24726,6 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true, - "optional": true - }, "node_modules/psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -27697,17 +24848,6 @@ } ] }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, "node_modules/qified": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", @@ -27931,93 +25071,6 @@ "node": ">=8" } }, - "node_modules/read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up/node_modules/path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up/node_modules/path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up/node_modules/read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/read-pkg/node_modules/type-fest": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", @@ -28072,21 +25125,6 @@ "node": ">= 0.10" } }, - "node_modules/redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha512-qtW5hKzGQZqKoh6JNSD+4lfitfPKGz42e6QwiRmPM5mmKtR0N41AbJRYu0xJi7nhOJ4WDgRkKvAk6tw4WIwR4g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -28141,19 +25179,6 @@ "@babel/runtime": "^7.8.4" } }, - "node_modules/regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "dependencies": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -28209,46 +25234,6 @@ "regjsparser": "bin/parser" } }, - "node_modules/repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "optional": true, - "dependencies": { - "is-finite": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -28435,13 +25420,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "deprecated": "https://github.com/lydell/resolve-url#deprecated", - "dev": true - }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -28451,16 +25429,6 @@ "node": ">=10" } }, - "node_modules/responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", - "dev": true, - "optional": true, - "dependencies": { - "lowercase-keys": "^1.0.0" - } - }, "node_modules/restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -28496,15 +25464,6 @@ "node": ">=4" } }, - "node_modules/ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true, - "engines": { - "node": ">=0.12" - } - }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -28762,15 +25721,6 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, - "node_modules/safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "dependencies": { - "ret": "~0.1.10" - } - }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -28930,20 +25880,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/seek-bzip": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", - "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", - "dev": true, - "optional": true, - "dependencies": { - "commander": "^2.8.1" - }, - "bin": { - "seek-bunzip": "bin/seek-bunzip", - "seek-table": "bin/seek-bzip-table" - } - }, "node_modules/select": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", @@ -28972,45 +25908,12 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", - "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", - "dev": true, - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/semver-truncate": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-1.1.2.tgz", - "integrity": "sha1-V/Qd5pcHpicJp+AQS6IRcQnqR+g=", - "dev": true, - "optional": true, - "dependencies": { - "semver": "^5.3.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/semver-truncate/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "optional": true, + "license": "ISC", "bin": { - "semver": "bin/semver" + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/send": { @@ -29225,46 +26128,6 @@ "node": ">= 0.4" } }, - "node_modules/set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/set-value/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/set-value/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -29272,31 +26135,6 @@ "dev": true, "license": "ISC" }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/shell-quote": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", @@ -29551,120 +26389,6 @@ "tslib": "^2.0.3" } }, - "node_modules/snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "dependencies": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "dependencies": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "dependencies": { - "kind-of": "^3.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/snapdragon/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -29726,33 +26450,6 @@ "node": ">= 14" } }, - "node_modules/sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-plain-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sort-keys-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", - "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", - "dev": true, - "optional": true, - "dependencies": { - "sort-keys": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -29792,20 +26489,6 @@ "webpack": "^5.72.1" } }, - "node_modules/source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", - "dev": true, - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -29825,13 +26508,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "deprecated": "See https://github.com/lydell/source-map-url#deprecated", - "dev": true - }, "node_modules/spdx-correct": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", @@ -29922,104 +26598,12 @@ "node": ">=8.0" } }, - "node_modules/split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, - "node_modules/squeak": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/squeak/-/squeak-1.3.0.tgz", - "integrity": "sha1-MwRQN7ZDiLVnZ0uEMiplIQc5FsM=", - "dev": true, - "optional": true, - "dependencies": { - "chalk": "^1.0.0", - "console-stream": "^0.1.1", - "lpad-align": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/squeak/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/squeak/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/squeak/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/squeak/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/squeak/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -30045,14 +26629,6 @@ "node": ">=0.10.0" } }, - "node_modules/stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", - "dev": true, - "optional": true - }, "node_modules/stable-hash-x": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", @@ -30075,47 +26651,21 @@ "node": ">=10" } }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/stackframe": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.1.tgz", - "integrity": "sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg==", - "dev": true - }, - "node_modules/static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "dependencies": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, + "node_modules/stackframe": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.1.tgz", + "integrity": "sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg==", + "dev": true + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -30168,16 +26718,6 @@ "text-decoder": "^1.1.0" } }, - "node_modules/strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -30390,26 +26930,6 @@ "node": ">=8" } }, - "node_modules/strip-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", - "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", - "dev": true, - "optional": true, - "dependencies": { - "is-natural-number": "^4.0.1" - } - }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -30419,23 +26939,6 @@ "node": ">=6" } }, - "node_modules/strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "get-stdin": "^4.0.1" - }, - "bin": { - "strip-indent": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -30447,26 +26950,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-outer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", - "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", - "dev": true, - "optional": true, - "dependencies": { - "escape-string-regexp": "^1.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strnum": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", - "dev": true, - "optional": true - }, "node_modules/style-search": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", @@ -31063,50 +27546,6 @@ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", "dev": true }, - "node_modules/svgo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", - "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", - "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", - "dev": true, - "optional": true, - "dependencies": { - "chalk": "^2.4.1", - "coa": "^2.0.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "^0.1.1", - "css-tree": "1.0.0-alpha.37", - "csso": "^4.0.2", - "js-yaml": "^3.13.1", - "mkdirp": "~0.5.1", - "object.values": "^1.1.0", - "sax": "~1.2.4", - "stable": "^0.1.8", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/svgo/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "optional": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -31284,42 +27723,6 @@ "streamx": "^2.12.5" } }, - "node_modules/temp-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", - "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", - "dev": true, - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/tempfile": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz", - "integrity": "sha512-ZOn6nJUgvgC09+doCEF3oB+r3ag7kUvlsXEGX069QRD60p+P3uP7XG9N2/at+EyIRGSN//ZY3LyEotA1YpmjuA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "temp-dir": "^1.0.0", - "uuid": "^3.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tempfile/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "optional": true, - "bin": { - "uuid": "bin/uuid" - } - }, "node_modules/terser": { "version": "5.37.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", @@ -31519,16 +27922,6 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, - "node_modules/timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/tiny-emitter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", @@ -31644,54 +28037,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "node_modules/to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", - "dev": true, - "optional": true - }, - "node_modules/to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "dependencies": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -31742,29 +28087,6 @@ "node": ">=18" } }, - "node_modules/trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/trim-repeated": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", - "dev": true, - "optional": true, - "dependencies": { - "escape-string-regexp": "^1.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -32123,21 +28445,6 @@ "node": ">=4" } }, - "node_modules/union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -32170,13 +28477,6 @@ "node": ">= 0.8" } }, - "node_modules/unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", - "dev": true, - "optional": true - }, "node_modules/unrs-resolver": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", @@ -32215,57 +28515,6 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" } }, - "node_modules/unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "isarray": "1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/update-browserslist-db": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", @@ -32333,13 +28582,6 @@ "node": ">= 0.10" } }, - "node_modules/urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "deprecated": "Please see https://github.com/lydell/urix#deprecated", - "dev": true - }, "node_modules/url-loader": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", @@ -32367,39 +28609,6 @@ } } }, - "node_modules/url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "prepend-http": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/url-to-options": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", - "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", - "dev": true, - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/use-memo-one": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", @@ -32415,22 +28624,6 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, - "node_modules/util.promisify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", - "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", - "dev": true, - "optional": true, - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.2", - "has-symbols": "^1.0.1", - "object.getownpropertydescriptors": "^2.1.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -33586,16 +29779,6 @@ "npm": ">=1.0.0" } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.4" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 9bb1fd5d0af5b..ab44fd105f344 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,23 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "ea285b45692aed6c4f95353671393402f97f0aa7", + "sha": "be6a50be6834e922b9db12c75c9a2cd817b2db59", "ghcrRepo": "WordPress/gutenberg/gutenberg-wp-develop-build" }, "engines": { - "node": ">=20.10.0", - "npm": ">=10.2.3" + "node": ">=24.18.0", + "npm": ">=11.16.0" + }, + "devEngines": { + "runtime": { + "name": "node", + "version": ">=24.18.0" + }, + "packageManager": { + "name": "npm", + "version": ">=11.16.0", + "onFail": "error" + } }, "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -28,9 +39,12 @@ "last 2 Opera versions" ], "devDependencies": { + "@343dev/gifsicle": "1.2.0", "@lodder/grunt-postcss": "^3.1.1", + "@napi-rs/image": "1.14.0", "@playwright/test": "1.61.1", "@pmmmwh/react-refresh-webpack-plugin": "0.6.2", + "@types/backbone": "1.4.23", "@types/codemirror": "5.60.17", "@types/espree": "10.1.0", "@types/htmlhint": "1.1.5", @@ -42,7 +56,6 @@ "@wordpress/scripts": "33.0.0", "autoprefixer": "10.5.3", "chalk": "5.6.2", - "check-node-version": "4.2.1", "cssnano": "7.1.9", "dotenv": "17.4.2", "dotenv-expand": "13.0.0", @@ -52,7 +65,6 @@ "grunt-contrib-concat": "2.1.0", "grunt-contrib-copy": "~1.0.0", "grunt-contrib-cssmin": "~5.0.0", - "grunt-contrib-imagemin": "~4.0.0", "grunt-contrib-jshint": "3.2.0", "grunt-contrib-uglify": "~5.2.2", "grunt-contrib-watch": "~1.1.0", @@ -99,10 +111,11 @@ "jquery-color": "3.0.0", "jquery-form": "4.3.0", "jquery-hoverintent": "1.10.2", + "jquery-migrate": "3.6.0", "jsonlint": "1.6.3", "lodash": "4.18.1", "masonry-layout": "4.2.2", - "moment": "2.30.1", + "moment": "2.31.0", "objectFitPolyfill": "2.3.5", "polyfill-library": "4.8.0", "react": "18.3.1", @@ -117,13 +130,13 @@ "zod": "3.25.1" }, "scripts": { - "build": "grunt build", - "build:dev": "grunt build --dev", - "build:gutenberg": "grunt build:gutenberg", - "dev": "grunt watch --dev", - "test": "grunt test", - "watch": "grunt watch", - "grunt": "grunt", + "build": "node --no-maglev --no-concurrent-sparkplug node_modules/grunt/bin/grunt build", + "build:dev": "node --no-maglev --no-concurrent-sparkplug node_modules/grunt/bin/grunt build --dev", + "build:gutenberg": "node --no-maglev --no-concurrent-sparkplug node_modules/grunt/bin/grunt build:gutenberg", + "dev": "node --no-maglev --no-concurrent-sparkplug node_modules/grunt/bin/grunt watch --dev", + "test": "node --no-maglev --no-concurrent-sparkplug node_modules/grunt/bin/grunt test", + "watch": "node --no-maglev --no-concurrent-sparkplug node_modules/grunt/bin/grunt watch", + "grunt": "node --no-maglev --no-concurrent-sparkplug node_modules/grunt/bin/grunt", "lint:jsdoc": "wp-scripts lint-js", "lint:jsdoc:fix": "wp-scripts lint-js --fix", "typecheck:js": "tsc --build", @@ -146,6 +159,6 @@ "typecheck:php:baselines": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan:baselines --", "gutenberg:copy": "node tools/gutenberg/copy.js", "gutenberg:verify": "node tools/gutenberg/utils.js", - "gutenberg:download": "node tools/gutenberg/download.js && grunt build:gutenberg" + "gutenberg:download": "node tools/gutenberg/download.js && npm run build:gutenberg" } } diff --git a/phpcompat.xml.dist b/phpcompat.xml.dist index c4ebab64ea19b..4ad70ff62e374 100644 --- a/phpcompat.xml.dist +++ b/phpcompat.xml.dist @@ -46,8 +46,8 @@ /src/wp-content/plugins/* - - /src/wp-content/themes/(?!twenty)* + + /src/wp-content/themes/(?!twenty|ipsum)* /src/wp-content/languages/* diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 5a62c500f3150..9f15c0c3670d6 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -112,8 +112,8 @@ /src/wp-content/plugins/* - - /src/wp-content/themes/(?!twenty)* + + /src/wp-content/themes/(?!twenty|ipsum)* /src/wp-content/languages/* diff --git a/phpstan.neon.dist b/phpstan.neon.dist index bb806bc4a4b95..3363599ff05fe 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -24,8 +24,6 @@ includes: # phpstan:baselines start - tests/phpstan/baselines/argument.type.neon - tests/phpstan/baselines/argument.unresolvableType.neon - - tests/phpstan/baselines/arguments.count.neon - - tests/phpstan/baselines/arrayValues.list.neon - tests/phpstan/baselines/assign.propertyType.neon - tests/phpstan/baselines/binaryOp.invalid.neon - tests/phpstan/baselines/booleanAnd.alwaysFalse.neon @@ -33,61 +31,40 @@ includes: - tests/phpstan/baselines/booleanAnd.rightAlwaysTrue.neon - tests/phpstan/baselines/booleanNot.alwaysFalse.neon - tests/phpstan/baselines/booleanNot.alwaysTrue.neon - - tests/phpstan/baselines/booleanOr.alwaysFalse.neon - - tests/phpstan/baselines/booleanOr.alwaysTrue.neon - - tests/phpstan/baselines/booleanOr.rightAlwaysTrue.neon - - tests/phpstan/baselines/catch.neverThrown.neon - tests/phpstan/baselines/class.notFound.neon - tests/phpstan/baselines/deadCode.unreachable.neon - tests/phpstan/baselines/empty.offset.neon - tests/phpstan/baselines/empty.property.neon - tests/phpstan/baselines/empty.variable.neon - - tests/phpstan/baselines/encapsedStringPart.nonString.neon - - tests/phpstan/baselines/foreach.nonIterable.neon - tests/phpstan/baselines/function.alreadyNarrowedType.neon - tests/phpstan/baselines/function.impossibleType.neon - tests/phpstan/baselines/function.resultUnused.neon - - tests/phpstan/baselines/greaterOrEqual.alwaysTrue.neon - tests/phpstan/baselines/identical.alwaysFalse.neon - tests/phpstan/baselines/identical.alwaysTrue.neon - tests/phpstan/baselines/if.alwaysFalse.neon - tests/phpstan/baselines/if.alwaysTrue.neon - - tests/phpstan/baselines/instanceof.alwaysTrue.neon - tests/phpstan/baselines/isset.offset.neon - tests/phpstan/baselines/isset.property.neon - - tests/phpstan/baselines/isset.variable.neon - tests/phpstan/baselines/method.childParameterType.neon - - tests/phpstan/baselines/method.nonObject.neon - - tests/phpstan/baselines/method.notFound.neon - tests/phpstan/baselines/method.unused.neon - tests/phpstan/baselines/notIdentical.alwaysTrue.neon - - tests/phpstan/baselines/nullCoalesce.offset.neon - tests/phpstan/baselines/nullCoalesce.property.neon - tests/phpstan/baselines/offsetAccess.nonOffsetAccessible.neon - tests/phpstan/baselines/offsetAccess.notFound.neon - tests/phpstan/baselines/offsetAssign.valueType.neon - tests/phpstan/baselines/parameter.defaultValue.neon - tests/phpstan/baselines/parameter.notFound.neon - - tests/phpstan/baselines/parameter.phpDocType.neon - tests/phpstan/baselines/parameterByRef.type.neon - tests/phpstan/baselines/parameterByRef.unusedType.neon - tests/phpstan/baselines/property.defaultValue.neon - tests/phpstan/baselines/property.nonObject.neon - tests/phpstan/baselines/property.notFound.neon - - tests/phpstan/baselines/property.onlyWritten.neon - - tests/phpstan/baselines/property.phpDocType.neon - tests/phpstan/baselines/property.private.neon - tests/phpstan/baselines/property.protected.neon - - tests/phpstan/baselines/return.missing.neon - tests/phpstan/baselines/return.type.neon - tests/phpstan/baselines/return.unusedType.neon - - tests/phpstan/baselines/smallerOrEqual.alwaysTrue.neon - - tests/phpstan/baselines/staticClassAccess.privateMethod.neon - - tests/phpstan/baselines/ternary.alwaysTrue.neon - tests/phpstan/baselines/varTag.noVariable.neon - tests/phpstan/baselines/variable.undefined.neon - - tests/phpstan/baselines/while.alwaysFalse.neon - - tests/phpstan/baselines/while.alwaysTrue.neon # phpstan:baselines end parameters: @@ -124,6 +101,22 @@ parameters: path: src/wp-includes/canonical.php count: 1 + # In core, @internal is advisory for extenders: it marks a symbol that plugins should not rely on. Core itself is + # one codebase and its own uses of these symbols are intended. PHPStan scopes @internal by root namespace, and + # since core symbols live in the global namespace, it reports every use outside the declaring class or file. + # + # The identifier depends on where the symbol is used (e.g. new.internalClass, parameter.internalInterface, + # varTag.internalTrait), so the entry matches on the message instead, which always names the symbol right after + # "internal ". The name must not contain a backslash, so uses of @internal symbols from namespaced + # third-party libraries like SimplePie and Requests are still reported. A bundled library in the global + # namespace cannot be told apart from core that way, so it must be excluded by its prefix instead. Currently + # that is only sodium_compat (ParagonIE_), which marks much of its API as @internal. These checks are only + # enabled by bleedingEdge; functions, class constants, instantiations, and static calls are checked from level 0, + # and methods, properties, and type references from level 2. + - + message: '#\binternal (?:static )?(?:class|interface|trait|enum|function|method|constant|property) (?!ParagonIE_)\w++(?!\\)#' + reportUnmatched: false + # Level 2: # ValueError is PHP 8.0+; core throws it conditionally so the docblocks are correct for WP's 7.4+ range, # but bleedingEdge's version-aware check treats the class as non-existent against the PHP 7.4 floor. @@ -131,3 +124,60 @@ parameters: message: '#^PHPDoc tag @throws with type InvalidArgumentException\|ValueError is not subtype of Throwable$#' path: src/wp-includes/compat.php reportUnmatched: false + + # Level 5: + # substr_compare()'s $length has always accepted null, meaning "compare the full length". PHP 7.4 spelled + # that `int $length = null`, where the null default makes the parameter implicitly nullable, and PHP 8.0 + # only made it explicit as `?int $length = null`. The behavior never differed: verified on PHP 7.4 that + # passing null compares the whole string rather than coercing to a length of 0. + # + # PHPStan reads the two spellings from two sources and only one is right. Its PHP 8 stub carries `?int`, + # but the pre-8.0 `resources/functionMap.php` records the type as plain `int`, having dropped the implicit + # nullability when it was transcribed from the old manual. Because `phpVersion.min` is 70400, the legacy + # map wins and null is reported as invalid. + # + # This lives here rather than in a baseline because a baseline entry records work still to be done, and + # there is none: the call is correct on every version WordPress supports, and the fault is in PHPStan's + # data. Passing an explicit length purely to satisfy it would change working code to suit a tooling bug. + # `reportUnmatched: false` so this entry lapses quietly if PHPStan corrects the map. + - + message: '#^Parameter \#4 \$length of function substr_compare expects int, null given\.$#' + path: src/wp-includes/html-api/class-wp-html-tag-processor.php + reportUnmatched: false + + # HTML API: + # These errors are ignored for the sake of making it easier to keep the HTML Processor in sync with the HTML spec. + - + message: '#^Comparison operation "\>" between 999\|1000 and 0 is always true\.$#' + identifier: greater.alwaysTrue + count: 1 + path: src/wp-includes/html-api/class-wp-html-processor.php + - + message: '#^Comparison operation "\>\=" between 0 and 8 is always false\.$#' + identifier: greaterOrEqual.alwaysFalse + count: 1 + path: src/wp-includes/html-api/class-wp-html-processor.php + - + message: '#^Left side of && is always true\.$#' + identifier: booleanAnd.leftAlwaysTrue + count: 1 + path: src/wp-includes/html-api/class-wp-html-processor.php + - + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable + count: 15 + path: src/wp-includes/html-api/class-wp-html-processor.php + - + message: '#^If condition is always true\.$#' + identifier: if.alwaysTrue + count: 2 + path: src/wp-includes/html-api/class-wp-html-processor.php + - + # The early return at the start of step() narrows $last_error to null, and PHPStan does not see that + # bookmark_token() sets it to ERROR_EXCEEDED_MAX_BOOKMARKS before throwing, whether it is called directly + # in step() or via insert_virtual_node() in a step_*() method. So it reports that the checks in both catch + # blocks of step() can never match, when in fact they do whenever bookmarks run out. + message: '#^Strict comparison using \=\=\= between ''exceeded\-max…'' and null will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 2 + path: src/wp-includes/html-api/class-wp-html-processor.php diff --git a/src/js/_enqueues/admin/application-passwords.js b/src/js/_enqueues/admin/application-passwords.js index c79cdb8b4037f..21fbeb65820af 100644 --- a/src/js/_enqueues/admin/application-passwords.js +++ b/src/js/_enqueues/admin/application-passwords.js @@ -2,6 +2,11 @@ * @output wp-admin/js/application-passwords.js */ +/** + * Handles the Application Passwords functionality in the user profile screen. + * + * @param {JQueryStatic} $ The jQuery object. + */ ( function( $ ) { var $appPassSection = $( '#application-passwords-section' ), $newAppPassForm = $appPassSection.find( '.create-application-password' ), @@ -167,8 +172,8 @@ * * @since 5.6.0 * - * @param {jqXHR} xhr The XHR object from the ajax call. - * @param {string} textStatus The string categorizing the ajax request's status. + * @param {jqXHR} xhr The XHR object from the ajax call. + * @param {string} textStatus The string categorizing the ajax request's status. * @param {string} errorThrown The HTTP status error text. */ function handleErrorResponse( xhr, textStatus, errorThrown ) { @@ -188,7 +193,7 @@ * * @param {string} message The message to display. * @param {string} type The notice type. Either 'success' or 'error'. - * @returns {jQuery} The notice element. + * @return {jQuery} The notice element. */ function addNotice( message, type ) { var $notice = $( '
' ) diff --git a/src/js/_enqueues/admin/comment.js b/src/js/_enqueues/admin/comment.js index b31f174bab5fe..5ed06b0fa41f7 100644 --- a/src/js/_enqueues/admin/comment.js +++ b/src/js/_enqueues/admin/comment.js @@ -9,7 +9,7 @@ * * @since 2.5.0 * - * @param {jQuery} $ The jQuery object. + * @param {JQueryStatic} $ The jQuery object. */ jQuery( function($) { @@ -24,8 +24,6 @@ jQuery( function($) { /** * Adds event that opens the time stamp form if the form is hidden. * - * @listens $edittimestamp:click - * * @param {Event} event The event object. * @return {void} */ @@ -43,8 +41,6 @@ jQuery( function($) { /** * Resets the time stamp values when the cancel button is clicked. * - * @listens .cancel-timestamp:click - * * @param {Event} event The event object. * @return {void} */ @@ -65,8 +61,6 @@ jQuery( function($) { /** * Sets the time stamp values when the ok button is clicked. * - * @listens .save-timestamp:click - * * @param {Event} event The event object. * @return {void} */ @@ -110,8 +104,6 @@ jQuery( function($) { * * @since 7.1.0 * - * @listens $editcommentparent:click - * * @param {Event} event The event object. * @return {void} */ @@ -131,8 +123,6 @@ jQuery( function($) { * * @since 7.1.0 * - * @listens .cancel-comment-parent:click - * * @param {Event} event The event object. * @return {void} */ @@ -150,8 +140,6 @@ jQuery( function($) { * * @since 7.1.0 * - * @listens .save-comment-parent:click - * * @param {Event} event The event object. * @return {void} */ diff --git a/src/js/_enqueues/admin/common.js b/src/js/_enqueues/admin/common.js index c3d49310e8897..43534fe60714b 100644 --- a/src/js/_enqueues/admin/common.js +++ b/src/js/_enqueues/admin/common.js @@ -2,15 +2,14 @@ * @output wp-admin/js/common.js */ -/* global setUserSetting, ajaxurl, alert, confirm, pagenow */ -/* global columns, screenMeta */ +/* global setUserSetting, ajaxurl, alert, confirm, pagenow, columns, screenMeta */ /** - * Adds common WordPress functionality to the window. + * Adds common WordPress functionality to the window. * - * @param {jQuery} $ jQuery object. - * @param {Object} window The window object. - * @param {mixed} undefined Unused. + * @param {JQueryStatic} $ The jQuery object. + * @param {Object} window The window object. + * @param {*} undefined Unused. */ ( function( $, window, undefined ) { var $document = $( document ), @@ -58,10 +57,10 @@ function deprecatedProperty( propName, version, replacement ) { * @since 5.6.0 Added the `version` parameter. * * @param {string} name The name of the object, i.e. commonL10n. - * @param {object} l10nObject The object to deprecate the properties on. + * @param {Object} l10nObject The object to deprecate the properties on. * @param {string} version The version of WordPress that deprecated the property. * - * @return {object} The object with all its properties deprecated. + * @return {Object} The object with all its properties deprecated. */ function deprecateL10nObject( name, l10nObject, version ) { var deprecatedObject = {}; @@ -476,8 +475,6 @@ window.columns = { * Gets the checked column toggles from the screen options. * * @since 3.0.0 - * - * @return {string} String containing the checked column names. */ useCheckboxesForHidden : function() { this.hidden = function(){ @@ -557,7 +554,7 @@ window.showNotice = { * * @since 2.7.0 * - * @param text The text to display in the message. + * @param {string} text The text to display in the message. */ note : function(text) { alert(text); @@ -570,8 +567,8 @@ window.showNotice = { * @since 3.2.0 * * @type {{element: null, toggles: null, page: null, init: screenMeta.init, - * toggleEvent: screenMeta.toggleEvent, open: screenMeta.open, - * close: screenMeta.close}} + * toggleEvent: screenMeta.toggleEvent, open: screenMeta.open, + * close: screenMeta.close}} * * @return {void} */ @@ -695,7 +692,7 @@ $('.contextual-help-tabs').on( 'click', 'a', function(e) { $('.contextual-help-tabs .active').removeClass('active'); link.parent('li').addClass('active'); - panel = $( link.attr('href') ); + panel = $( document ).find( link.attr('href') ); // Panels. $('.help-tab-content').not( panel ).removeClass('active').hide(); @@ -982,8 +979,8 @@ $( function() { /* * Show the sub instead of following the link if: - * - the submenu is not open. - * - the submenu is not shown inline or the menu is not folded. + * - the submenu is not open. + * - the submenu is not shown inline or the menu is not folded. */ if ( ! $menuItem.hasClass( 'opensub' ) && ( ! $menuItem.hasClass( 'wp-menu-open' ) || $menuItem.width() < 40 ) ) { event.preventDefault(); @@ -1263,6 +1260,7 @@ $( function() { /** * Triggers the primary submit when then secondary submit is clicked. * + * @param {SubmitEvent} e The event object. * @since 5.7.0 * * @return {void} @@ -1486,7 +1484,7 @@ $( function() { * @param {Event} e The event object. * * @return {void} - */ + */ $('#contextual-help-link, #show-settings-link').on( 'focus.scroll-into-view', function(e){ if ( e.target.scrollIntoViewIfNeeded ) e.target.scrollIntoViewIfNeeded(false); @@ -1845,7 +1843,7 @@ $( function() { $document.on( 'postbox-toggled', this.maybeDisableSortables ); // When the screen columns are changed, potentially disable sortables. - $( '#screen-options-wrap input' ).on( 'click', this.maybeDisableSortables ); + $( '#screen-options-wrap input:not(.meta-box-reordering-toggle)' ).on( 'click', this.maybeDisableSortables ); }, /** @@ -1859,6 +1857,7 @@ $( function() { var width = navigator.userAgent.indexOf('AppleWebKit/') > -1 ? $window.width() : window.innerWidth; if ( + $body.hasClass( 'meta-box-reordering-disabled' ) || ( width <= 782 ) || ( 1 >= $sortables.find( '.ui-sortable-handle:visible' ).length && jQuery( '.columns-prefs-1 input' ).prop( 'checked' ) ) ) { @@ -2245,6 +2244,8 @@ $( function( $ ) { * plugin icon images in the update plugins table. * * @since 6.4.0 + * + * @return {Object} Public methods. */ (function() { // Private variables and methods. diff --git a/src/js/_enqueues/admin/custom-background.js b/src/js/_enqueues/admin/custom-background.js index f83db00af03b4..b10a72296d2c6 100644 --- a/src/js/_enqueues/admin/custom-background.js +++ b/src/js/_enqueues/admin/custom-background.js @@ -10,6 +10,8 @@ * @since 3.0.0 * * @requires jQuery + * + * @param {JQueryStatic} $ The jQuery object. */ (function($) { $( function() { @@ -122,7 +124,7 @@ * @since 3.5.0 * * @return {void} - */ + */ frame.on( 'select', function() { // Grab the selected attachment. var attachment = frame.state().get('selection').first(); diff --git a/src/js/_enqueues/admin/custom-header.js b/src/js/_enqueues/admin/custom-header.js index 367756ef8758b..40f9be4f35962 100644 --- a/src/js/_enqueues/admin/custom-header.js +++ b/src/js/_enqueues/admin/custom-header.js @@ -11,6 +11,8 @@ * * @deprecated 4.1.0 The page this is used on is never linked to from the UI. * Setting a custom header is completely handled by the Customizer. + * + * @param {JQueryStatic} $ The jQuery object. */ (function($) { var frame; diff --git a/src/js/_enqueues/admin/edit-comments.js b/src/js/_enqueues/admin/edit-comments.js index 78ee78de759ff..7e89f1c0dd7cb 100644 --- a/src/js/_enqueues/admin/edit-comments.js +++ b/src/js/_enqueues/admin/edit-comments.js @@ -1,14 +1,15 @@ +/* global adminCommentsSettings, thousandsSeparator, list_args, QTags, ajaxurl, wpAjax */ +/* global commentReply, theExtraList, theList, setCommentsList */ + /** - * Handles updating and editing comments. - * - * @file This file contains functionality for the admin comments page. - * @since 2.1.0 * @output wp-admin/js/edit-comments.js */ -/* global adminCommentsSettings, thousandsSeparator, list_args, QTags, ajaxurl, wpAjax */ -/* global commentReply, theExtraList, theList, setCommentsList */ - +/** + * Handles updating and editing comments. + * + * @param {JQueryStatic} $ The jQuery object. + */ (function($) { var getCount, updateCount, updateCountText, updatePending, updateApproved, updateHtmlTitle, updateDashboardText, updateInModerationText, adminTitle = document.title, @@ -41,7 +42,7 @@ var getCount, updateCount, updateCountText, updatePending, updateApproved, * @access private * * @param {jQuery} el The jQuery element to update. - * @param {number} n Number to be put in the element. + * @param {number} n Number to be put in the element. * * @return {void} */ @@ -67,7 +68,7 @@ var getCount, updateCount, updateCountText, updatePending, updateApproved, * @since 4.4.0 * @access private * - * @param {number} diff The amount to lower or raise the approved count with. + * @param {number} diff The amount to lower or raise the approved count with. * @param {number} commentPostId The ID of the post to be updated. * * @return {void} @@ -121,7 +122,7 @@ var getCount, updateCount, updateCountText, updatePending, updateApproved, * * @param {string} selector The jQuery selector for elements to update a count * for. - * @param {number} diff The amount to lower or raise the count with. + * @param {number} diff The amount to lower or raise the count with. * * @return {void} */ @@ -230,7 +231,7 @@ var getCount, updateCount, updateCountText, updatePending, updateApproved, * @since 3.2.0 * @access private * - * @param {number} diff The amount to lower or raise the pending count with. + * @param {number} diff The amount to lower or raise the pending count with. * @param {number} commentPostId The ID of the post to be updated. * * @return {void} @@ -321,9 +322,9 @@ window.setCommentsList = function() { * @since 2.8.0 * @access private * - * @param {number} total Total number of comments. - * @param {number} time Unix timestamp of response. - * @param {boolean} setConfidentTime Whether to update the last confident time + * @param {number} total Total number of comments. + * @param {number} time Unix timestamp of response. + * @param {boolean} setConfidentTime Whether to update the last confident time * with the given time. * * @return {void} @@ -344,7 +345,7 @@ window.setCommentsList = function() { * @since 2.5.0 * @access private * - * @param {Object} r Ajax response object. + * @param {Object} r Ajax response object. * @param {Object} settings Settings for the wpList object. * * @return {void} @@ -397,8 +398,8 @@ window.setCommentsList = function() { * @since 2.8.0 * @access private * - * @param {Object} settings Settings for the wpList object. - * @param {HTMLElement} list Comments table element. + * @param {Object} settings Settings for the wpList object. + * @param {HTMLElement} list Comments table element. * * @return {Object} The settings object. */ @@ -470,7 +471,7 @@ window.setCommentsList = function() { * @since 2.5.0 * @access private * - * @param {Object} r Ajax response object. + * @param {Object} r Ajax response object. * @param {Object} settings Settings for the wpList object. * * @return {void} @@ -944,8 +945,8 @@ window.commentReply = { * @memberof commentReply * * @param {number} comment_id The comment ID to open an editor for. - * @param {number} post_id The post ID to open an editor for. - * @param {string} action The action to perform. Either 'edit' or 'replyto'. + * @param {number} post_id The post ID to open an editor for. + * @param {string} action The action to perform. Either 'edit' or 'replyto'. * * @return {boolean} Always false. */ diff --git a/src/js/_enqueues/admin/inline-edit-post.js b/src/js/_enqueues/admin/inline-edit-post.js index 6e6e3bef606ed..aaf4bcc5bc3ec 100644 --- a/src/js/_enqueues/admin/inline-edit-post.js +++ b/src/js/_enqueues/admin/inline-edit-post.js @@ -18,8 +18,11 @@ window.wp = window.wp || {}; * * @type {Object} * - * @property {string} type The type of inline editor. - * @property {string} what The prefix before the post ID. + * @property {string} type The type of inline editor. + * @property {string} what The prefix before the post ID. + * + * @param {JQueryStatic} $ The jQuery object. + * @param {wp} wp The WordPress global object. * */ ( function( $, wp ) { @@ -188,6 +191,8 @@ window.wp = window.wp || {}; * @since 2.7.0 * * @memberof inlineEditPost + * + * @return {void|false} Returns false if no checkboxes are checked, otherwise does not return anything. */ setBulk : function(){ var te = '', type = this.type, c = true; diff --git a/src/js/_enqueues/admin/inline-edit-tax.js b/src/js/_enqueues/admin/inline-edit-tax.js index 86e3498cd1eac..f22633651a70c 100644 --- a/src/js/_enqueues/admin/inline-edit-tax.js +++ b/src/js/_enqueues/admin/inline-edit-tax.js @@ -13,9 +13,11 @@ window.wp = window.wp || {}; * * @namespace inlineEditTax * - * @property {string} type The type of inline edit we are currently on. - * @property {string} what The type property with a hash prefixed and a dash - * suffixed. + * @property {string} type The type of inline edit we are currently on. + * @property {string} what The type property with a hash prefixed and a dash suffixed. + * + * @param {JQueryStatic} $ The jQuery object. + * @param {Object} wp The WordPress object. */ ( function( $, wp ) { @@ -27,7 +29,7 @@ window.inlineEditTax = { * * @since 2.7.0 * - * @this inlineEditTax + * @this {inlineEditTax} * @memberof inlineEditTax * @return {void} */ @@ -91,7 +93,7 @@ window.inlineEditTax = { * * @since 2.7.0 * - * @this inlineEditTax + * @this {inlineEditTax} * @memberof inlineEditTax * * @param {HTMLElement} el An element within the table row or the table row @@ -109,12 +111,12 @@ window.inlineEditTax = { * * @since 2.7.0 * - * @this inlineEditTax + * @this {inlineEditTax} * @memberof inlineEditTax * * @param {string|HTMLElement} id The ID of the term we want to quick edit or an * element within the table row or the - * table row itself. + * table row itself. * @return {boolean} Always returns false. */ edit : function(id) { @@ -156,12 +158,12 @@ window.inlineEditTax = { * * @since 2.7.0 * - * @this inlineEditTax + * @this {inlineEditTax} * @memberof inlineEditTax * * @param {string|HTMLElement} id The ID of the term we want to quick edit or an * element within the table row or the - * table row itself. + * table row itself. * @return {boolean} Always returns false. */ save : function(id) { @@ -253,7 +255,7 @@ window.inlineEditTax = { * * @since 2.7.0 * - * @this inlineEditTax + * @this {inlineEditTax} * @memberof inlineEditTax * @return {void} */ diff --git a/src/js/_enqueues/admin/link.js b/src/js/_enqueues/admin/link.js index ed4bd5e3efb94..b916021327160 100644 --- a/src/js/_enqueues/admin/link.js +++ b/src/js/_enqueues/admin/link.js @@ -30,7 +30,7 @@ jQuery( function($) { $(this).attr( 'aria-selected', 'true' ).removeAttr( 'tabindex' ); $(this).parent().addClass('tabs').siblings('li').removeClass('tabs'); $('.tabs-panel').hide(); - $(t).show(); + $( document ).find( t ).show(); if ( '#categories-all' == t ) { deleteUserSetting('cats'); } else { diff --git a/src/js/_enqueues/admin/media-frame-upload.js b/src/js/_enqueues/admin/media-frame-upload.js new file mode 100644 index 0000000000000..f5af17d7991d2 --- /dev/null +++ b/src/js/_enqueues/admin/media-frame-upload.js @@ -0,0 +1,271 @@ +/** + * Routes media frame uploads through the client-side media pipeline. + * + * Every wp.media frame - the Media Library grid on wp-admin/upload.php and + * the media modal the block editor, the site editor and the block widgets + * screen open - uploads via wp.Uploader / plupload to async-upload.php. + * When the browser is cross-origin isolated and supports the client-side + * pipeline, this script intercepts the uploader's FilesAdded handler and + * hands files to wp.mediaUploadPipeline (media-upload-pipeline.js) + * instead: the original image is uploaded via the REST API and thumbnails + * are generated in the browser (wasm-vips), then sideloaded and finalized. + * + * The frame's own UI is reused: the same placeholder tiles, progress bars, + * "Uploading n/m" status, and error list that wp-plupload.js drives. + * + * When client-side support is unavailable the script cleanly no-ops and + * the classic plupload flow is left untouched. + * + * @output wp-admin/js/media-frame-upload.js + */ + +/* global plupload */ + +( function () { + // Guard against double execution (e.g. duplicate enqueues). + if ( window.__wpMediaFrameUpload ) { + return; + } + + const pipeline = window.wp && wp.mediaUploadPipeline; + + // Bail unless the browser actually supports client-side processing. This + // is the clean no-op: when the isolation headers did not land, classic + // plupload keeps handling uploads. + if ( + ! pipeline || + typeof plupload === 'undefined' || + ! wp.Uploader || + ! wp.media || + ! pipeline.configure() + ) { + return; + } + + window.__wpMediaFrameUpload = true; + + /** + * Resets the upload queue once every attachment has finished uploading. + * + * Parity with wp-plupload.js so browse mode flips back when done. + */ + function maybeResetQueue() { + const complete = wp.Uploader.queue.all( function ( + /** @type {WPAttachment} */ attachment + ) { + return ! attachment.get( 'uploading' ); + } ); + + if ( complete ) { + wp.Uploader.queue.reset(); + } + } + + /** + * Handles a completed upload by syncing the grid tile with the server data. + * + * @param {WPUploader} wpUploader The wp.Uploader instance that queued the file. + * @param {WPAttachment} model The placeholder Attachment model. + * @param {PipelineAttachment} attachment The finalized attachment from the pipeline. + */ + function handleSuccess( wpUploader, model, attachment ) { + // Not silent: Attachments re-keys a model from its cid to its id when + // it sees the change event. Without that the collection cannot find the + // attachment by id, and refreshing the library duplicates its tile. + model.set( { id: attachment.id } ); + + // Register the model in Attachments.all (parity with wp-plupload.js). + wp.media.model.Attachment.get( attachment.id, model ); + + // Clear the uploading state before the refetch lands, not after. The + // attachment details view re-renders on the refetch's changes, so it has + // to see `uploading: false` by then or it keeps showing a progress bar. + // wp-plupload.js sets both in one call for the same reason. + [ 'file', 'loaded', 'size', 'percent' ].forEach( function ( key ) { + model.unset( key, { silent: true } ); + } ); + model.set( { uploading: false }, { silent: true } ); + + model + .fetch() + .fail( function () { + // Fetch failed, but the upload succeeded: fill the tile with what + // the pipeline gave us so it is not left empty. + model.set( attachment ); + } ) + .always( function () { + maybeResetQueue(); + + // Parity with wp-plupload.js, which exposes this callback so + // other code can react to a finished upload. + wpUploader.success( model ); + } ); + } + + /** + * Handles an upload error by removing the tile and surfacing the message. + * + * The error goes into wp.Uploader.errors exactly like a classic upload + * error, so the grid's error sidebar renders it, announces it, and + * moves focus to its Dismiss button. + * + * @param {WPUploader} wpUploader The wp.Uploader instance that queued the file. + * @param {WPAttachment} model The placeholder Attachment model. + * @param {UploadError} error The upload error. + * @param {string} fileName Name of the file (for the error label). + */ + function handleError( wpUploader, model, error, fileName ) { + const message = pipeline.getErrorText( error, fileName ); + const file = { name: fileName }; + + model.destroy(); + + wp.Uploader.errors.unshift( { + message: message, + data: {}, + file: file, + } ); + + maybeResetQueue(); + + // Parity with wp-plupload.js, which exposes this callback so other + // code can react to a failed upload. + wpUploader.error( message, {}, file ); + } + + /** + * Whether a plupload uploader posts attachments the way wp.Uploader does + * by default. + * + * The init wrapper below reaches every wp.Uploader built on the page, + * and a plugin can build one for its own purposes. Core's defaults tell + * an attachment upload apart: it posts to async-upload.php with the + * `upload-attachment` action. Anything a plugin pointed elsewhere, or + * gave another action, is uploading something else and is left alone. + * + * @param {plupload.Uploader} up The plupload uploader instance. + * @return {boolean} True when the uploader posts attachments. + */ + function isAttachmentUploader( up ) { + const settings = up.settings || {}; + const params = settings.multipart_params || {}; + const url = new URL( settings.url || '', location.href ); + + return ( + 'upload-attachment' === params.action && + url.pathname.endsWith( 'async-upload.php' ) + ); + } + + /** + * Intercepts files added to a plupload uploader. + * + * Builds the same placeholder tiles as wp-plupload for the files the + * pipeline takes (see wp.mediaUploadPipeline.routeBatch()). Returns + * undefined to leave the whole batch to plupload's built-in handler. + * + * @param {WPUploader} wpUploader The wp.Uploader instance. + * @param {plupload.Uploader} up The plupload uploader instance. + * @param {plupload.File[]} files Files added to the queue. + * @return {boolean|undefined} False to suppress the built-in handler. + */ + function handleFilesAdded( wpUploader, up, files ) { + if ( ! isAttachmentUploader( up ) ) { + return; + } + + // The classic flow attaches to `post_id` the same way, and rejects + // a post the user cannot edit just as the REST API does. + const params = ( up.settings && up.settings.multipart_params ) || {}; + + return pipeline.routeBatch( up, files, { + postId: parseInt( params.post_id, 10 ) || 0, + onQueue: function ( + /** @type {plupload.File} */ file, + /** @type {File} */ nativeFile + ) { + // Build the same placeholder attributes as wp-plupload.js so + // the grid's progress tiles and "Uploading n/m" status work + // unchanged. + /** @type {PlaceholderAttributes} */ + const attributes = { + file: file, + uploading: true, + date: new Date(), + filename: file.name, + menuOrder: 0, + uploadedTo: wp.media.model.settings.post.id, + loaded: file.loaded, + size: file.size, + percent: file.percent, + }; + + /* + * Early mime type scanning for images, as wp-plupload.js does, + * extended with the formats the client-side pipeline accepts. + */ + const image = /(?:jpe?g|png|gif|webp|avif|heic|heif)$/i.exec( + file.name + ); + if ( image ) { + attributes.type = 'image'; + // `jpg` is not a valid subtype, so map it to `jpeg`. + const subtype = image[ 0 ].toLowerCase(); + attributes.subtype = 'jpg' === subtype ? 'jpeg' : subtype; + } + + const model = wp.media.model.Attachment.create( attributes ); + wp.Uploader.queue.add( model ); + wpUploader.added( model ); + + return { + onSuccess: function ( + /** @type {PipelineAttachment} */ attachment + ) { + handleSuccess( wpUploader, model, attachment ); + }, + onError: function ( /** @type {UploadError} */ error ) { + handleError( wpUploader, model, error, nativeFile.name ); + }, + // plupload builds its own tile for the file it took over. + onHandOff: function () { + model.destroy(); + }, + onProgress: function ( /** @type {number} */ percent ) { + model.set( { percent: percent } ); + wpUploader.progress( model ); + }, + }; + }, + } ); + } + + // Wrap wp.Uploader.prototype.init (an empty stub called once per instance + // after plupload is initialized) to bind a higher-priority FilesAdded + // handler on every uploader instance: the grid's, and the one each media + // modal creates when it opens. + const originalInit = wp.Uploader.prototype.init; + wp.Uploader.prototype.init = function () { + originalInit.apply( this, arguments ); + + const wpUploader = /** @type {WPUploader} */ ( this ); + const up = /** @type {plupload.Uploader|undefined} */ ( this.uploader ); + + if ( ! up || up.__wpMediaFrameUploadBound ) { + return; + } + up.__wpMediaFrameUploadBound = true; + + // plupload sorts handlers by priority (descending) and a `false` + // return breaks the chain, so priority 100 runs before and suppresses + // the built-in FilesAdded handler. + up.bind( + 'FilesAdded', + function ( uploader, files ) { + return handleFilesAdded( wpUploader, uploader, files ); + }, + this, + 100 + ); + }; +} )(); diff --git a/src/js/_enqueues/admin/media-new-upload.js b/src/js/_enqueues/admin/media-new-upload.js new file mode 100644 index 0000000000000..e48c6900b8357 --- /dev/null +++ b/src/js/_enqueues/admin/media-new-upload.js @@ -0,0 +1,339 @@ +/** + * Routes "Add New Media File" uploads through the client-side media pipeline. + * + * On wp-admin/media-new.php WordPress uploads via a raw plupload.Uploader + * (created by plupload-handlers) posting to async-upload.php. When the + * browser is cross-origin isolated and supports the client-side pipeline, + * this script intercepts the uploader's FilesAdded handler and hands files + * to wp.mediaUploadPipeline (media-upload-pipeline.js) instead: the + * original image is uploaded via the REST API and thumbnails are generated + * in the browser (wasm-vips), then sideloaded and finalized. + * + * The screen's existing UI helpers from plupload-handlers are reused: + * fileQueued() builds the progress item, uploadSuccess() renders the + * finished attachment row (via the async-upload.php markup endpoint), and + * uploadComplete() runs when the queue drains. Failed uploads render the + * same notice async-upload.php returns for a server-side failure, so the + * screen looks and behaves unchanged. + * + * When client-side support is unavailable the script cleanly no-ops and + * the classic plupload flow is left untouched. + * + * @output wp-admin/js/media-new-upload.js + */ + +/* global plupload, pluploadL10n, wpUploaderInit, fileQueued, uploadStart, uploadSuccess, uploadComplete */ + +( function () { + // Guard against double execution (e.g. duplicate enqueues). + if ( window.__wpMediaNewUpload ) { + return; + } + + const pipeline = window.wp && wp.mediaUploadPipeline; + + // Bail unless the browser actually supports client-side processing. This + // is the clean no-op: when the isolation headers did not land, classic + // plupload keeps handling uploads. + if ( + ! pipeline || + typeof plupload === 'undefined' || + ! wp.a11y || + ! pipeline.configure() + ) { + return; + } + + window.__wpMediaNewUpload = true; + + const __ = wp.i18n.__; + const sprintf = wp.i18n.sprintf; + + // Number of pipeline uploads currently in flight, used to fire + // uploadComplete() once both the pipeline and plupload are done. + let inFlightCount = 0; + + /** + * Marks one pipeline upload as finished, firing uploadComplete() when + * nothing is left uploading. + * + * The built-in UploadComplete binding never fires for pipeline uploads + * because every file is removed from plupload before its queue starts. + * Files plupload is still uploading (a batch's classic share, a HEIC + * handed back) end with plupload's own UploadComplete instead. + * + * @param {plupload.Uploader} up The plupload uploader. + */ + function finishUpload( up ) { + inFlightCount--; + if ( inFlightCount === 0 && up.state !== plupload.STARTED ) { + uploadComplete(); + } + } + + /** + * Returns the post the screen attaches uploads to, or 0. + * + * media-new.php validates `post_id` (the post must exist and be + * editable by the user) before printing it into the form, whereas the + * raw request value is what plupload carries in its multipart params. + * async-upload.php re-validates that raw value and silently uploads + * unattached; the REST API rejects it instead, so read the validated one. + * + * @return {number} The parent post ID, or 0. + */ + function getParentPostId() { + const input = /** @type {HTMLInputElement|null} */ ( + document.getElementById( 'post_id' ) + ); + const postId = input ? parseInt( input.value, 10 ) : 0; + return postId > 0 ? postId : 0; + } + + /** + * Returns the screen's progress item for a plupload file. + * + * @param {plupload.File} file The plupload file. + * @return {HTMLElement|null} The item fileQueued() built, if it is still on the page. + */ + function getItem( file ) { + return document.getElementById( 'media-item-' + file.id ); + } + + /** + * Renders a failed upload the way async-upload.php does for a + * server-side failure: an error notice with a real Dismiss button + * described by the notice, a screen reader announcement, and focus + * returned to the browse button once dismissed. + * + * @param {plupload.File} file The plupload file that failed. + * @param {string} message The reason the upload failed. + */ + function renderError( file, message ) { + const item = getItem( file ); + if ( ! item ) { + return; + } + + const descriptionId = 'error-description-' + file.id; + + const button = document.createElement( 'button' ); + button.type = 'button'; + button.id = 'dismiss-' + file.id; + button.className = 'dismiss button-link'; + button.setAttribute( 'aria-describedby', descriptionId ); + button.textContent = pluploadL10n.dismiss; + + const heading = document.createElement( 'strong' ); + heading.textContent = pluploadL10n.error_uploading.replace( + '%s', + file.name + ); + + const notice = document.createElement( 'div' ); + notice.id = descriptionId; + notice.className = 'notice notice-error error-div error'; + notice.append( + button, + ' ', + heading, + document.createElement( 'br' ), + message + ); + + item.replaceChildren( notice ); + // Read by itemAjaxError() in plupload-handlers so a later server + // error for the same file is not rendered twice. + item.dataset.lastErr = file.id; + + setTimeout( function () { + wp.a11y.speak( + sprintf( + /* translators: %s: Name of the file that failed to upload. */ + __( '%s has failed to upload.' ), + file.name + ) + ); + }, 1500 ); + + button.addEventListener( 'click', function () { + item.remove(); + wp.a11y.speak( __( 'Error dismissed.' ) ); + const browseButton = document.getElementById( + 'plupload-browse-button' + ); + if ( browseButton ) { + browseButton.focus(); + } + } ); + } + + /** + * Reflects pipeline progress onto the screen's progress bar for a file. + * + * The bar is 200px wide at 100%, matching uploadProgress() in + * plupload-handlers. + * + * @param {plupload.File} file The plupload file being uploaded. + * @param {number} percent Progress percentage. + */ + function renderProgress( file, percent ) { + const item = getItem( file ); + if ( ! item ) { + return; + } + + const bar = /** @type {HTMLElement|null} */ ( + item.querySelector( '.bar' ) + ); + if ( bar ) { + bar.style.width = 2 * percent + 'px'; + } + + const label = item.querySelector( '.percent' ); + if ( label ) { + label.textContent = percent + '%'; + } + } + + /** + * Intercepts files added to the plupload uploader. + * + * Builds the screen's progress items for the files the pipeline takes + * (see wp.mediaUploadPipeline.routeBatch()). Returns undefined to leave + * the whole batch to plupload's built-in handler. + * + * @param {plupload.Uploader} up The plupload uploader instance. + * @param {plupload.File[]} files Files added to the queue. + * @return {boolean|undefined} False to suppress the built-in handler. + */ + function handleFilesAdded( up, files ) { + return pipeline.routeBatch( up, files, { + // Without the parent post a file uploaded from + // media-new.php?post_id=N would land unattached even though the + // screen counts it against that post. + postId: getParentPostId(), + onStart: function ( /** @type {plupload.File[]} */ queued ) { + // Parity with the built-in handler: clear stale queue errors + // and run the shared upload-start housekeeping. + const queueErrors = document.getElementById( 'media-upload-error' ); + if ( queueErrors ) { + queueErrors.replaceChildren(); + } + uploadStart(); + + // Count the whole batch first: addItems() can reject a file + // synchronously, which must not drain the count mid-batch. + inFlightCount += queued.length; + }, + onQueue: function ( + /** @type {plupload.File} */ file, + /** @type {File} */ nativeFile + ) { + // Build the screen's progress item for this file. + fileQueued( file ); + + return { + onSuccess: function ( + /** @type {PipelineAttachment} */ attachment + ) { + // uploadSuccess() renders the finished attachment row + // via the existing async-upload.php markup endpoint; + // the server normally returns the ID as a string. + uploadSuccess( file, String( attachment.id ) ); + finishUpload( up ); + }, + onError: function ( /** @type {UploadError} */ error ) { + renderError( + file, + pipeline.getErrorText( error, nativeFile.name ) + ); + finishUpload( up ); + }, + // plupload builds its own progress item for the file it + // took over, so this one goes away. + onHandOff: function () { + const item = getItem( file ); + if ( item ) { + item.remove(); + } + finishUpload( up ); + }, + onProgress: function ( /** @type {number} */ percent ) { + renderProgress( file, percent ); + }, + }; + }, + } ); + } + + /** + * Binds the interceptor to an uploader once it has initialized. + * + * plupload sorts handlers by priority (descending) and a `false` return + * breaks the chain, so priority 100 runs before and suppresses the + * built-in FilesAdded and UploadComplete handlers. + * + * @param {plupload.Uploader} up The uploader that initialized. + */ + function bindUploader( up ) { + if ( up.__wpMediaNewUploadBound ) { + return; + } + up.__wpMediaNewUploadBound = true; + + up.bind( + 'FilesAdded', + function ( up, files ) { + return handleFilesAdded( up, files ); + }, + null, + 100 + ); + + // Hold back the built-in uploadComplete() while pipeline uploads + // are still running; finishUpload() fires it once they are done. + up.bind( + 'UploadComplete', + function () { + return inFlightCount > 0 ? false : undefined; + }, + null, + 100 + ); + } + + // plupload-handlers creates its uploader from the `wpUploaderInit` + // settings once the DOM is ready, after this script has run, and + // plupload binds the handlers in a settings `init` map while the + // uploader initializes. Hooking PostInit there reaches the uploader + // without depending on ready-callback ordering. The settings are + // missing when the browser uploader is disabled (the html-uploader + // fallback), in which case there is nothing to intercept. + if ( typeof wpUploaderInit !== 'object' || ! wpUploaderInit ) { + return; + } + + // Either shape may already hold another script's handlers, which keep + // running ahead of the interceptor being bound. + const init = wpUploaderInit.init; + if ( typeof init === 'function' ) { + wpUploaderInit.init = function ( up ) { + init( up ); + bindUploader( up ); + }; + } else { + const postInit = init && init.PostInit; + wpUploaderInit.init = Object.assign( {}, init, { + PostInit: function ( + /** @type {plupload.Uploader} */ up, + /** @type {unknown[]} */ ...args + ) { + if ( typeof postInit === 'function' ) { + postInit.call( this, up, ...args ); + } + bindUploader( up ); + }, + } ); + } +} )(); diff --git a/src/js/_enqueues/admin/media-upload-pipeline.js b/src/js/_enqueues/admin/media-upload-pipeline.js new file mode 100644 index 0000000000000..58dceae7affcc --- /dev/null +++ b/src/js/_enqueues/admin/media-upload-pipeline.js @@ -0,0 +1,739 @@ +/** + * Shared glue for routing classic admin uploads through the client-side + * media pipeline. + * + * The media frames (media-frame-upload.js) and the Add New Media + * File screen (media-new-upload.js) both intercept plupload and hand files + * to @wordpress/upload-media instead. Everything that is not specific to + * one screen's UI lives here: feature detection, configuring the + * upload-media store once, the REST sideload/finalize/delete helpers the + * store needs, queueing a file with success, error, and progress + * callbacks, the error text shown for a failed upload, and the guard that + * warns before leaving while uploads are in flight. + * + * Exposed as wp.mediaUploadPipeline. + * + * @output wp-admin/js/media-upload-pipeline.js + */ + +/* global plupload */ + +window.wp = window.wp || {}; + +( function ( wp ) { + const __ = wp.i18n.__; + const settings = window._wpMediaUploadPipelineSettings || {}; + /** @type {any} */ + let uploadStore; + let configured = false; + + // Uploads in flight, keyed by the onSuccess callback queueFile() passes + // to addItems(). The store keeps that callback on the queue item, so it + // identifies the item even after HEIC conversion swaps its file. + const entries = new Map(); + + // Files handed back to classic plupload after the pipeline gave up on + // them, so the interceptors let plupload's own handler take them. + const classicFiles = new WeakSet(); + + const imageSizeCount = Object.keys( settings.allImageSizes || {} ).length; + + /** + * Recursively appends data to a FormData object, supporting nested objects. + * + * Mirrors flattenFormData() in the media-utils package. + * + * @param {FormData} formData The form data to append to. + * @param {string} key The key to append under. + * @param {*} data The value to append. + */ + function flattenFormData( formData, key, data ) { + if ( + data !== null && + typeof data === 'object' && + Object.getPrototypeOf( data ) === Object.prototype + ) { + Object.keys( data ).forEach( function ( name ) { + flattenFormData( formData, key + '[' + name + ']', data[ name ] ); + } ); + } else if ( data !== undefined ) { + formData.append( key, String( data ) ); + } + } + + /** + * Sideloads a client-generated thumbnail to an existing attachment. + * + * Reimplements the private sideloadMedia() helper from the media-utils + * package as a thin apiFetch wrapper. + * + * @param {Object} args The sideload arguments. + * @param {File} args.file The sub-size to sideload. + * @param {number} args.attachmentId The attachment to sideload it to. + * @param {Record} [args.additionalData] Extra fields to send with it. + * @param {AbortSignal} [args.signal] Signal aborting the request. + * @param {( subSize: Object ) => void} [args.onSuccess] Called with the sideloaded sub-size. + * @param {( error: Error ) => void} [args.onError] Called when the sideload failed. + */ + function mediaSideload( args ) { + const file = args.file; + const additionalData = args.additionalData || {}; + + const data = new FormData(); + data.append( 'file', file, file.name || file.type.replace( '/', '.' ) ); + Object.keys( additionalData ).forEach( function ( key ) { + flattenFormData( data, key, additionalData[ key ] ); + } ); + + wp.apiFetch( { + path: '/wp/v2/media/' + args.attachmentId + '/sideload', + body: data, + method: 'POST', + signal: args.signal, + } ) + .then( function ( /** @type {Object} */ subSize ) { + if ( args.onSuccess ) { + args.onSuccess( subSize ); + } + } ) + .catch( function ( /** @type {unknown} */ error ) { + if ( args.onError ) { + let normalized = error; + if ( ! ( error instanceof Error ) ) { + const rest = /** @type {UploadError} */ ( error ); + normalized = new Error( + rest && rest.message ? rest.message : String( error ) + ); + } + args.onError( /** @type {Error} */ ( normalized ) ); + } + } ); + } + + /** + * Finalizes an upload once all client-side processing is complete. + * + * Reimplements the private mediaFinalize() helper. The returned + * attachment is load-bearing: it carries the post-finalize (scaled) + * URL used for srcset. + * + * @param {number} id The parent attachment ID. + * @param {Object[]} subSizes Accumulated sub-size data. + * @return {Promise} Resolves with the transformed attachment. + */ + async function mediaFinalize( id, subSizes ) { + /** @type {Object|undefined} */ + const response = await wp.apiFetch( { + path: '/wp/v2/media/' + id + '/finalize', + method: 'POST', + data: { sub_sizes: subSizes || [] }, + } ); + if ( ! response ) { + return undefined; + } + return wp.mediaUtils.transformAttachment( response ); + } + + /** + * Deletes an attachment whose client-side processing failed outright. + * + * The queue calls this when every sub-size sideload for an upload fails: + * without it the original file is left behind as an attachment with no + * metadata, visible in the Media Library after the next page load. The + * block editor passes the same setting. + * + * @param {number} id The attachment ID to delete. + * @return {Promise} Resolves once the attachment is deleted. + */ + function mediaDelete( id ) { + return wp.apiFetch( { + path: '/wp/v2/media/' + id + '?force=true', + method: 'DELETE', + } ); + } + + /** + * Whether every script the pipeline relies on is present. + * + * @return {boolean} True when the dependencies loaded. + */ + function hasDependencies() { + return Boolean( + typeof plupload !== 'undefined' && + wp.uploadMedia && + wp.mediaUtils && + wp.data && + wp.element && + wp.apiFetch + ); + } + + /** + * Whether the browser can run the client-side pipeline on this page. + * + * False when the isolation headers did not land (the page is not + * cross-origin isolated) or the browser lacks the required features, + * in which case classic plupload keeps handling uploads. + * + * @return {boolean} True when client-side processing is available. + */ + function isSupported() { + return Boolean( + hasDependencies() && + wp.uploadMedia.detectClientSideMediaSupport && + wp.uploadMedia.detectClientSideMediaSupport().supported + ); + } + + /** + * Estimates the progress (0-100) of a queue item. + * + * The pipeline never reports a numeric `progress` on its queue items + * (nothing dispatches updateItemProgress), so estimate one from the + * item's operation queue instead: each finished operation (prepare, + * transcode, upload, thumbnails, finalize) advances the bar, and the + * sub-sizes sideloaded so far advance it within thumbnail generation. + * `item.progress` is preferred whenever it is present. + * + * @param {QueueItem} item The upload-media queue item. + * @param {UploadEntry} entry The pipeline's bookkeeping for the upload. + * @return {number} Estimated progress. + */ + function estimateProgress( item, entry ) { + if ( typeof item.progress === 'number' ) { + return item.progress; + } + + const remaining = item.operations ? item.operations.length : 0; + let totals = entry.totals; + if ( ! totals ) { + totals = { total: remaining, remaining: remaining }; + entry.totals = totals; + } + // Operations are appended after preparation, so grow the total. + if ( remaining > totals.remaining ) { + totals.total += remaining - totals.remaining; + } + totals.remaining = remaining; + + if ( totals.total === 0 ) { + return 0; + } + + const completed = totals.total - remaining; + let fraction = 0; + if ( + 'THUMBNAIL_GENERATION' === item.currentOperation && + imageSizeCount > 0 + ) { + fraction = Math.min( + 1, + ( item.subSizes || [] ).length / imageSizeCount + ); + } + + return ( ( completed + fraction ) / totals.total ) * 100; + } + + /** + * Tracks queued uploads on every change to the upload-media store. + * + * Reports progress, which holds at 99 until the upload's success + * callback has run so nothing looks finished before the screen has + * synced the result, and notes the attachment once the original is + * uploaded. Sub-size children are skipped; only top-level items matter. + * + * The store can also remove an item without calling either of its + * callbacks (it only calls onSuccess when the item has an attachment), + * so an upload that was seen in the store and is gone fails here + * instead of staying in flight, and holding the unload guard, for good. + */ + function onStoreChange() { + if ( entries.size === 0 ) { + return; + } + + const present = new Set(); + const items = wp.data.select( uploadStore ).getItems(); + items.forEach( function ( /** @type {QueueItem} */ item ) { + if ( item.parentId ) { + return; + } + + const entry = entries.get( item.onSuccess ); + if ( ! entry ) { + return; + } + + present.add( item.onSuccess ); + entry.seen = true; + if ( item.attachment && item.attachment.id ) { + entry.attachmentId = item.attachment.id; + } + + if ( ! entry.onProgress ) { + return; + } + + const percent = Math.min( + 99, + Math.round( estimateProgress( item, entry ) ) + ); + if ( percent !== entry.lastPercent ) { + entry.lastPercent = percent; + entry.onProgress( percent ); + } + } ); + + entries.forEach( function ( entry, key ) { + if ( entry.seen && ! present.has( key ) ) { + entry.fail( { + message: __( 'An error occurred while uploading the file.' ), + } ); + } + } ); + } + + /** + * Whether something else already configured the store on this page. + * + * The block editor renders its own MediaUploadProvider into the same + * store. Neither `mediaSideload` nor `mediaFinalize` has a default, so + * their presence proves a provider's settings landed, whichever one + * rendered them. + * + * @return {boolean} True when the store carries a provider's settings. + */ + function isStoreConfigured() { + const storeSettings = wp.data.select( uploadStore ).getSettings(); + return Boolean( + storeSettings && + storeSettings.mediaSideload && + storeSettings.mediaFinalize + ); + } + + /** + * Configures the upload-media store for this page, once. + * + * Rendering the provider with useSubRegistry: false wires the settings + * into the store that wp.data.dispatch/select address. The block editor + * does the same for its own uploads, so on its screens no second + * provider is rendered: whichever committed last would win, and this + * one could replace the editor's mediaUpload. Uploads wait for the + * editor's settings instead (see isReady()). + * + * @return {boolean} True when the pipeline is configured and usable. + */ + function configure() { + if ( configured ) { + return true; + } + + if ( ! isSupported() ) { + return false; + } + + configured = true; + uploadStore = wp.uploadMedia.store; + + if ( settings.isBlockEditor || isStoreConfigured() ) { + wp.data.subscribe( onStoreChange, uploadStore ); + addUnloadGuard(); + return true; + } + + /* + * The media-utils package branches on this flag: without it + * uploadMedia() creates and revokes a throwaway blob URL per file + * and emits an extra onFileChange carrying it. The block editor + * sets the same flag before configuring the same pipeline. + */ + window.__clientSideMediaProcessing = true; + + wp.element + .createRoot( document.createElement( 'div' ) ) + .render( + wp.element.createElement( wp.uploadMedia.MediaUploadProvider, { + settings: { + mediaUpload: wp.mediaUtils.uploadMedia, + mediaSideload: mediaSideload, + mediaFinalize: mediaFinalize, + mediaDelete: mediaDelete, + maxUploadFileSize: settings.maxUploadFileSize, + allowedMimeTypes: settings.allowedMimeTypes, + allImageSizes: settings.allImageSizes, + bigImageSizeThreshold: settings.bigImageSizeThreshold, + imageStripMeta: settings.imageStripMeta, + imageMaxBitDepth: settings.imageMaxBitDepth, + generateAnimatedImageSubsizes: + settings.generateAnimatedImageSubsizes, + }, + useSubRegistry: false, + } ) + ); + + // Only the upload-media store can change what this listener reads. + wp.data.subscribe( onStoreChange, uploadStore ); + addUnloadGuard(); + + return true; + } + + /** + * Warns before leaving while uploads are in flight. + * + * Thumbnails that have not been sideloaded yet are lost and the + * attachment is left unfinalized, unlike classic uploads that complete + * server-side once the bytes arrive. + */ + function addUnloadGuard() { + window.addEventListener( 'beforeunload', function ( event ) { + if ( entries.size > 0 ) { + event.preventDefault(); + } + } ); + } + + /** + * Whether the store has received a provider's settings. + * + * A provider renders asynchronously, so a file added before its + * settings land must be left to classic plupload - a degradation, + * never data loss. The store's default state already carries a no-op + * `mediaUpload`, so only settings a provider alone supplies prove the + * real ones have landed: a file queued before that would be handed to + * the no-op and never upload. On block editor screens only the + * editor's provider supplies them. + * + * @return {boolean} True when the store is ready to accept files. + */ + function isReady() { + return configured && isStoreConfigured(); + } + + /** + * Splits a batch of plupload files between the pipeline and classic + * plupload. + * + * Audio files stay classic so they keep the title and description that + * media_handle_upload() derives from their ID3 tags, which the REST + * endpoint does not do. So do JPEG and PNG images while plupload's own + * client-side resize is enabled (the "Scale images" setting, or a + * `resize` passed through `plupload_init`): plupload scales those down + * before uploading them, the pipeline would not. + * + * The whole batch stays classic when a file cannot be re-queued on + * plupload or is already plupload's: plupload returns no native File + * for sources it cannot expose as one (the html4 runtime, say), and a + * file the pipeline handed back (see handOffToClassic()) is plupload's + * to upload. + * + * @param {plupload.Uploader} up The plupload uploader the files were added to. + * @param {plupload.File[]} files Files added to the plupload queue. + * @return {{ pipeline: plupload.File[], classic: plupload.File[] }|null} The split, + * or null when the whole batch stays on the classic path. + */ + function partitionBatch( up, files ) { + const resize = up.settings && up.settings.resize; + const isResizing = Boolean( resize && resize.enabled ); + /** @type {{ pipeline: plupload.File[], classic: plupload.File[] }} */ + const batch = { pipeline: [], classic: [] }; + + for ( const file of files ) { + if ( plupload.FAILED === file.status ) { + continue; + } + const nativeFile = file.getNative && file.getNative(); + if ( ! nativeFile || classicFiles.has( nativeFile ) ) { + return null; + } + const type = file.type || ''; + if ( + /^audio\//i.test( type ) || + ( isResizing && /^image\/(?:jpeg|png)$/i.test( type ) ) + ) { + batch.classic.push( file ); + } else { + batch.pipeline.push( file ); + } + } + + return batch.pipeline.length ? batch : null; + } + + /** + * Hands a file to classic plupload, whose own FilesAdded handler queues + * and starts the upload. + * + * @param {plupload.Uploader} up The plupload uploader. + * @param {File} nativeFile The file to upload. + */ + function addClassicFile( up, nativeFile ) { + classicFiles.add( nativeFile ); + up.addFile( nativeFile ); + } + + /** + * Re-queues the classic share of a batch on plupload. + * + * Suppressing plupload's built-in FilesAdded handler covers the whole + * batch, so the files partitionBatch() left to plupload are removed and + * added back on their own. + * + * @param {plupload.Uploader} up The plupload uploader. + * @param {plupload.File[]} files The files to upload classically. + */ + function handOffBatchToClassic( up, files ) { + files.forEach( function ( file ) { + // partitionBatch() already established that every file has one. + const nativeFile = /** @type {File} */ ( file.getNative() ); + up.removeFile( file ); + addClassicFile( up, nativeFile ); + } ); + } + + /** + * Re-queues a file on classic plupload when the server can do what the + * browser could not. + * + * The pipeline converts HEIC in the browser and fails outright when the + * platform has no HEIC decoder (Linux, and Windows without the HEVC + * extension), whereas the classic upload lets the server convert it. + * When the server's image editor supports HEIC - plupload carries no + * `heic_upload_error` setting then - the file goes back to plupload, + * whose own FilesAdded handler queues and starts the classic upload. + * Nothing else can convert it, so any other failure stands. + * + * @param {plupload.Uploader} up The plupload uploader the file came from. + * @param {UploadError} error Why the pipeline gave up on it. + * @param {File} nativeFile The file the pipeline was given. + * @return {boolean} True when the file was handed to classic plupload. + */ + function handOffToClassic( up, error, nativeFile ) { + const errorCodes = wp.uploadMedia.ErrorCode || {}; + if ( + ! error || + ! errorCodes.HEIC_DECODE_ERROR || + error.code !== errorCodes.HEIC_DECODE_ERROR || + ! up || + typeof up.addFile !== 'function' || + ( up.settings && up.settings.heic_upload_error ) + ) { + return false; + } + + addClassicFile( up, nativeFile ); + return true; + } + + /** + * Builds the extra fields to send with an upload from plupload's + * multipart params. + * + * Anything a plugin added through the `plupload_default_params` filter + * or wp.Uploader.param() reached the classic upload as $_POST fields, + * so it is forwarded to the REST request the same way. The classic + * transport's own fields are dropped: `action` and `_wpnonce` belong to + * async-upload.php, and `post_id` is spelled `post` by the REST API and + * passed separately after the screen has validated it. + * + * @param {Record} params Plupload's multipart_params. + * @param {number} postId The validated post to attach the upload to, or 0. + * @return {Record} Additional data for the upload. + */ + function additionalDataFromParams( params, postId ) { + /** @type {Record} */ + const additionalData = {}; + Object.keys( params || {} ).forEach( function ( key ) { + if ( 'action' === key || '_wpnonce' === key || 'post_id' === key ) { + return; + } + additionalData[ key ] = params[ key ]; + } ); + if ( postId ) { + additionalData.post = postId; + } + return additionalData; + } + + /** + * Queues a file for client-side processing and upload. + * + * @param {File} nativeFile The file to upload. + * @param {Record} additionalData Extra fields to send with the attachment. + * @param {QueueFileCallbacks} [callbacks] Lifecycle callbacks. + */ + function queueFile( nativeFile, additionalData, callbacks = {} ) { + /** + * Handles successful upload. + * + * @param {PipelineAttachment[]} attachments The uploaded attachments. + */ + function onSuccess( attachments ) { + entries.delete( onSuccess ); + if ( callbacks.onSuccess ) { + callbacks.onSuccess( attachments[ 0 ] ); + } + } + + /** @type {UploadEntry} */ + const entry = { + onProgress: callbacks.onProgress, + lastPercent: -1, + totals: null, + seen: false, + attachmentId: 0, + fail: function ( error ) { + if ( ! entries.delete( onSuccess ) ) { + return; + } + // The original already reached the server (finalize failed, + // say), so delete it: otherwise a retry leaves a duplicate + // next to an attachment whose metadata was never finalized. + // The store only deletes it itself when every sub-size failed. + if ( entry.attachmentId ) { + mediaDelete( entry.attachmentId ).catch( function () {} ); + } + if ( callbacks.onError ) { + callbacks.onError( error ); + } + }, + }; + entries.set( onSuccess, entry ); + + wp.data.dispatch( uploadStore ).addItems( { + files: [ nativeFile ], + additionalData: additionalData || {}, + onSuccess: onSuccess, + onError: function ( /** @type {UploadError|string} */ error ) { + // Also reached for files addItems() rejects before queueing. + // The block editor's mediaUpload() setting, which the store + // uses once the editor has configured it, reports transport + // failures as a bare message string. + entry.fail( + typeof error === 'string' ? { message: error } : error + ); + }, + } ); + } + + /** + * Routes a batch of files added to a plupload uploader through the + * pipeline. + * + * Returns undefined (not false) when the pipeline cannot take the batch + * so plupload's built-in FilesAdded handler runs and uploads it + * server-side - a degradation, never data loss. Otherwise queues the + * files the pipeline can take, re-queues the rest on plupload, and + * returns false to suppress the built-in handler, which would otherwise + * start a classic upload of every file. + * + * The classic flow posts plupload's multipart params to + * async-upload.php, so they are forwarded to the REST request for + * plugins reading $_POST, with `post_id` replaced by `options.postId`. + * + * @param {plupload.Uploader} up The plupload uploader the files were added to. + * @param {plupload.File[]} files Files added to the queue. + * @param {RouteBatchOptions} options The screen's hooks. + * @return {boolean|undefined} False to suppress the built-in handler. + */ + function routeBatch( up, files, options ) { + if ( ! isReady() ) { + return; + } + + const batch = partitionBatch( up, files ); + if ( ! batch ) { + return; + } + + const params = ( up.settings && up.settings.multipart_params ) || {}; + const additionalData = additionalDataFromParams( + params, + options.postId + ); + + if ( options.onStart ) { + options.onStart( batch.pipeline ); + } + + batch.pipeline.forEach( function ( /** @type {plupload.File} */ file ) { + // partitionBatch() already established that every file has one. + const nativeFile = /** @type {File} */ ( file.getNative() ); + const callbacks = options.onQueue( file, nativeFile ); + + // Remove the file from plupload so it is not uploaded twice. + up.removeFile( file ); + + queueFile( nativeFile, additionalData, { + onSuccess: callbacks.onSuccess, + onProgress: callbacks.onProgress, + onError: function ( /** @type {UploadError} */ error ) { + // When the server can convert what the browser could + // not, plupload takes the file from here and shows its + // own progress. + if ( handOffToClassic( up, error, nativeFile ) ) { + callbacks.onHandOff(); + return; + } + callbacks.onError( error ); + }, + } ); + } ); + + handOffBatchToClassic( up, batch.classic ); + + up.refresh(); + + return false; + } + + /** + * Builds the display text for a failed upload. + * + * wp.uploadMedia.getErrorMessage() maps an error *code* and a file name + * to a { title, description, action } object, so it can neither be handed + * the Error itself nor used as a string. Only codes it actually maps are + * worth using: its fallback (and the GENERAL code) says nothing the + * error's own message does not, and preferring the message there keeps a + * server-supplied reason instead of replacing it with "Please try again." + * + * @param {UploadError} error The upload error. + * @param {string} fileName Name of the file that failed to upload. + * @return {string} A human-readable message. + */ + function getErrorText( error, fileName ) { + const errorCodes = wp.uploadMedia.ErrorCode || {}; + const code = error && error.code; + let details; + + if ( + code && + code !== errorCodes.GENERAL && + Object.prototype.hasOwnProperty.call( errorCodes, code ) && + wp.uploadMedia.getErrorMessage + ) { + details = wp.uploadMedia.getErrorMessage( code, fileName ); + + if ( details && details.description ) { + return details.action ? + details.description + ' ' + details.action : + details.description; + } + } + + return ( + ( error && error.message ) || + __( 'An error occurred while uploading the file.' ) + ); + } + + wp.mediaUploadPipeline = { + isSupported: isSupported, + configure: configure, + isReady: isReady, + routeBatch: routeBatch, + getErrorText: getErrorText, + }; +} )( window.wp ); diff --git a/src/js/_enqueues/admin/media.js b/src/js/_enqueues/admin/media.js index db61220b50e5f..f464fe960b03f 100644 --- a/src/js/_enqueues/admin/media.js +++ b/src/js/_enqueues/admin/media.js @@ -24,7 +24,7 @@ * @memberOf findPosts * * @param {string} af_name The name of the affected element. - * @param {string} af_val The value of the affected post element. + * @param {string} af_val The value of the affected post element. * * @return {boolean} Always returns false. */ diff --git a/src/js/_enqueues/admin/password-toggle.js b/src/js/_enqueues/admin/password-toggle.js index 5bfaa3b122c01..bdd98f8bb7359 100644 --- a/src/js/_enqueues/admin/password-toggle.js +++ b/src/js/_enqueues/admin/password-toggle.js @@ -1,10 +1,12 @@ +/** + * @output wp-admin/js/password-toggle.js + */ + /** * Adds functionality for password visibility buttons to toggle between text and password input types. * * @since 6.3.0 - * @output wp-admin/js/password-toggle.js */ - ( function () { var toggleElements, status, input, icon, label, __ = wp.i18n.__; @@ -15,6 +17,9 @@ toggle.addEventListener( 'click', togglePassword ); } ); + /** + * Toggles the password input field visibility. + */ function togglePassword() { status = this.getAttribute( 'data-toggle' ); input = this.parentElement.children.namedItem( 'pwd' ); diff --git a/src/js/_enqueues/admin/plugin-install.js b/src/js/_enqueues/admin/plugin-install.js index 1e994763e1fda..61e6a2e1ed60b 100644 --- a/src/js/_enqueues/admin/plugin-install.js +++ b/src/js/_enqueues/admin/plugin-install.js @@ -6,6 +6,11 @@ /* global tb_click, tb_remove, tb_position */ +/** + * Initializes the plugin install screens. + * + * @param {JQueryStatic} $ The jQuery object. + */ jQuery( function( $ ) { var tbWindow, @@ -78,6 +83,9 @@ jQuery( function( $ ) { $focusedBefore.trigger( 'focus' ); }); + /** + * Initializes the plugin details modal dialog after the iframe has fully loaded. + */ function iframeLoaded() { var $iframe = tbWindow.find( '#TB_iframeContent' ); @@ -109,8 +117,9 @@ jQuery( function( $ ) { }); } - /* - * Get the tabbable elements and detach/attach the keydown event. + /** + * Gets the tabbable elements and detaches/attaches the keydown event. + * * Called after the iframe has fully loaded so we have all the elements we need. * Called again each time a Tab gets clicked. * @todo Consider to implement a WordPress general utility for this and don't use jQuery UI. @@ -133,7 +142,11 @@ jQuery( function( $ ) { }); } - // Constrain tabbing within the plugin modal dialog. + /** + * Constrains tabbing within the plugin modal dialog. + * + * @param {JQuery.Event} event The keydown event. + */ function constrainTabbing( event ) { if ( 9 !== event.which ) { return; @@ -148,11 +161,13 @@ jQuery( function( $ ) { } } - /* - * Open the Plugin details modal. The event is delegated to get also the links - * in the plugins search tab, after the Ajax search rebuilds the HTML. It's - * delegated on the closest ancestor and not on the body to avoid conflicts - * with other handlers, see Trac ticket #43082. + /** + * Opens the Plugin details modal. + * + * The event is delegated to get also the links in the plugins search tab, + * after the Ajax search rebuilds the HTML. It's delegated on the closest + * ancestor and not on the body to avoid conflicts with other handlers, see + * Trac ticket #43082. */ $( '.wrap' ).on( 'click', '.thickbox.open-plugin-details-modal', function( e ) { // The `data-title` attribute is used only in the Plugin screens. @@ -203,7 +218,7 @@ jQuery( function( $ ) { // Flip the content. $( '#section-holder div.section' ).hide(); // Hide 'em all. - $( '#section-' + tab ).show(); + $( '#section-' + CSS.escape( tab ) ).show(); }); /* diff --git a/src/js/_enqueues/admin/post.js b/src/js/_enqueues/admin/post.js index 445e79d8a6f05..1c166a82cb32a 100644 --- a/src/js/_enqueues/admin/post.js +++ b/src/js/_enqueues/admin/post.js @@ -14,6 +14,11 @@ window.makeSlugeditClickable = window.editPermalink = function(){}; // Make sure the wp object exists. window.wp = window.wp || {}; +/** + * Handles the dynamic functionality needed on post and term pages. + * + * @param {JQueryStatic} $ The jQuery object. + */ ( function( $ ) { var titleHasFocus = false, __ = wp.i18n.__; @@ -253,7 +258,9 @@ window.wp = window.wp || {}; }(jQuery)); /** - * Heartbeat refresh nonces. + * Handles the Heartbeat refresh nonces. + * + * @param {JQueryStatic} $ The jQuery object. */ (function($) { var check, timeout; @@ -299,7 +306,9 @@ window.wp = window.wp || {}; }(jQuery)); /** - * All post and postbox controls and functionality. + * Handles all post and postbox controls and functionality. + * + * @param {JQueryStatic} $ The jQuery object. */ jQuery( function($) { var stamp, visibility, $submitButtons, updateVisibility, updateText, @@ -577,7 +586,7 @@ jQuery( function($) { $(this).attr( 'aria-selected', 'true' ).removeAttr( 'tabindex' ); $(this).parent().addClass('tabs').siblings('li').removeClass('tabs'); $('#' + taxonomy + '-tabs').siblings('.tabs-panel').hide(); - $(t).show(); + $( document ).find( t ).show(); if ( '#' + taxonomy + '-all' == t ) { deleteUserSetting( settingName ); } else { @@ -620,11 +629,11 @@ jQuery( function($) { }); /** - * Before adding a new taxonomy, disable submit button. + * Disables the submit button before adding a new taxonomy. * * @param {Object} s Taxonomy object which will be added. * - * @return {Object} + * @return {Object} Taxonomy object with additional data to be sent to the server. */ catAddBefore = function( s ) { if ( !$('#new'+taxonomy).val() ) { @@ -1321,7 +1330,10 @@ jQuery( function($) { } ); /** - * TinyMCE word count display + * Handles the TinyMCE word count display. + * + * @param {JQueryStatic} $ The jQuery object. + * @param {wp.utils.WordCounter} counter The WordCounter object. */ ( function( $, counter ) { $( function() { diff --git a/src/js/_enqueues/admin/postbox.js b/src/js/_enqueues/admin/postbox.js index b6d1b6569dc84..fec46d3f6d909 100644 --- a/src/js/_enqueues/admin/postbox.js +++ b/src/js/_enqueues/admin/postbox.js @@ -104,6 +104,10 @@ postboxWithinSortablesIndex = postboxesWithinSortables.index( postbox ), firstOrLastPositionMessage; + if ( ! postboxes.metaBoxReorderingEnabled ) { + return; + } + if ( 'dashboard_browser_nag' === postboxId ) { return; } @@ -246,10 +250,10 @@ * * @memberof postboxes * - * @param {string} page The page we are currently on. - * @param {Object} [args] - * @param {Function} args.pbshow A callback that is called when a postbox opens. - * @param {Function} args.pbhide A callback that is called when a postbox closes. + * @param {string} page The page we are currently on. + * @param {Object} [args] Optional arguments for the postbox toggles. + * @param {Function} [args.pbshow] A callback that is called when a postbox opens. + * @param {Function} [args.pbhide] A callback that is called when a postbox closes. * @return {void} */ add_postbox_toggles : function (page, args) { @@ -344,6 +348,13 @@ postboxes.save_order( page ); } }); + + $( '.meta-box-reordering-toggle' ).on( 'click.postboxes', function() { + var enabled = $( this ).prop( 'checked' ); + + postboxes.setMetaBoxReordering( enabled ); + postboxes.save_meta_box_reordering_state( enabled ); + } ); }, /** @@ -353,8 +364,8 @@ * * @memberof postboxes * - * @param {string} page The page we are currently on. - * @param {Object} [args={}] The arguments for the postbox initializer. + * @param {string} page The page we are currently on. + * @param {Object} [args={}] The arguments for the postbox initializer. * @param {Function} args.pbshow A callback that is called when a postbox opens. * @param {Function} args.pbhide A callback that is called when a postbox * closes. @@ -419,6 +430,8 @@ } }); + this.setMetaBoxReordering( this.isMetaBoxReorderingEnabled() ); + if ( isMobile ) { $(document.body).on('orientationchange.postboxes', function(){ postboxes._pb_change(); }); this._pb_change(); @@ -437,6 +450,51 @@ }); }, + /** + * Checks whether meta box reordering is enabled. + * + * @since 7.2.0 + * + * @return {boolean} Whether meta box reordering is enabled. + */ + isMetaBoxReorderingEnabled: function() { + var $toggle = $( '#meta-box-reordering' ); + + if ( $toggle.length ) { + return $toggle.prop( 'checked' ); + } + + return ! $( document.body ).hasClass( 'meta-box-reordering-disabled' ); + }, + + /** + * Enables or disables the meta box reordering UI. + * + * @since 7.2.0 + * + * @param {boolean} enabled Whether reordering should be enabled. + * @return {void} + */ + setMetaBoxReordering: function( enabled ) { + var $sortables = $( '.meta-box-sortables' ); + + this.metaBoxReorderingEnabled = enabled; + + $( document.body ).toggleClass( 'meta-box-reordering-disabled', ! this.metaBoxReorderingEnabled ); + + if ( window.wpResponsive && window.wpResponsive.maybeDisableSortables ) { + window.wpResponsive.maybeDisableSortables(); + return; + } + + if ( $sortables.hasClass( 'ui-sortable' ) ) { + $sortables + .sortable( this.metaBoxReorderingEnabled ? 'enable' : 'disable' ) + .find( '.ui-sortable-handle' ) + .toggleClass( 'is-non-sortable', ! this.metaBoxReorderingEnabled ); + } + }, + /** * Saves the state of the postboxes to the server. * @@ -476,6 +534,32 @@ ); }, + /** + * Saves the meta box reordering setting to the server. + * + * @since 7.2.0 + * + * @param {boolean} enabled Whether meta box reordering is enabled. + * @return {void} + */ + save_meta_box_reordering_state: function( enabled ) { + $.post( + ajaxurl, + { + action: 'meta-box-reordering', + enabled: enabled ? 1 : 0, + screenoptionnonce: $( '#screenoptionnonce' ).val() + }, + function() { + wp.a11y.speak( + enabled ? + __( 'Rearranging screen elements enabled.' ) : + __( 'Rearranging screen elements disabled.' ) + ); + } + ); + }, + /** * Saves the order of the postboxes to the server. * diff --git a/src/js/_enqueues/admin/privacy-tools.js b/src/js/_enqueues/admin/privacy-tools.js index 480766ad051d7..1a48c0759329a 100644 --- a/src/js/_enqueues/admin/privacy-tools.js +++ b/src/js/_enqueues/admin/privacy-tools.js @@ -1,27 +1,55 @@ /** - * Interactions used by the User Privacy tools in WordPress. - * * @output wp-admin/js/privacy-tools.js */ -// Privacy request action handling. +/** + * Sets the interactions used by the User Privacy tools in WordPress. + * + * @param {jQueryStatic} $ The jQuery object. + */ jQuery( function( $ ) { var __ = wp.i18n.__, copiedNoticeTimeout; + /** + * Sets the state of the action. + * + * @since 4.9.6 + * + * @param {jQuery} $action The action to set the state for. + * @param {string} state The state to set the action to. + * @return {void} + */ function setActionState( $action, state ) { $action.children().addClass( 'hidden' ); $action.children( '.' + state ).removeClass( 'hidden' ); } + /** + * Clears any results row after the request row. + * + * @since 4.9.6 + * + * @param {jQuery} $requestRow The request row to clear results for. + * @return {void} + */ function clearResultsAfterRow( $requestRow ) { - $requestRow.removeClass( 'has-request-results' ); - if ( $requestRow.next().hasClass( 'request-results' ) ) { $requestRow.next().remove(); } } + /** + * Appends a results row after the request row. + * + * @since 4.9.6 + * + * @param {jQuery} $requestRow The request row to append the results after. + * @param {string} classes The classes to add to the results row. + * @param {string} summaryMessage The summary message to display in the results row. + * @param {string[]} additionalMessages Additional messages to display in the results row. + * @return {void} + */ function appendResultsAfterRow( $requestRow, classes, summaryMessage, additionalMessages ) { var itemList = '', resultRowClasses = 'request-results'; @@ -35,8 +63,6 @@ jQuery( function( $ ) { itemList = '
    ' + itemList + '
'; } - $requestRow.addClass( 'has-request-results' ); - if ( $requestRow.hasClass( 'status-request-confirmed' ) ) { resultRowClasses = resultRowClasses + ' status-request-confirmed'; } @@ -51,7 +77,7 @@ jQuery( function( $ ) { '

' + summaryMessage + '

' + itemList + '' + - '' + + '' + ''; }); } @@ -76,6 +102,15 @@ jQuery( function( $ ) { clearResultsAfterRow( $requestRow ); setExportProgress( 0 ); + /** + * Handles a successful export. + * + * @since 4.9.6 + * + * @param {string} [zipUrl] Optional. The URL of the generated ZIP file. Undefined when + * the export link is sent by email instead. + * @return {void} + */ function onExportDoneSuccess( zipUrl ) { var summaryMessage = __( 'This user’s personal data export link was sent.' ); @@ -96,6 +131,15 @@ jQuery( function( $ ) { setTimeout( function() { $rowActions.removeClass( 'processing' ); }, 500 ); } + /** + * Handles an export failure. + * + * @since 4.9.6 + * + * @param {string} [errorMessage] Optional. The error message to display. The results + * row is only added when a message is passed. + * @return {void} + */ function onExportFailure( errorMessage ) { var summaryMessage = __( 'An error occurred while attempting to export personal data.' ); @@ -108,6 +152,15 @@ jQuery( function( $ ) { setTimeout( function() { $rowActions.removeClass( 'processing' ); }, 500 ); } + /** + * Updates the export progress indicator based on the number of completed exporters. + * + * @since 5.4.0 + * + * @param {number} exporterIndex The number of exporters that have finished. Should be + * between 0 and exportersCount. + * @return {void} + */ function setExportProgress( exporterIndex ) { var progress = ( exportersCount > 0 ? exporterIndex / exportersCount : 0 ), progressString = Math.round( progress * 100 ).toString() + '%'; @@ -115,6 +168,16 @@ jQuery( function( $ ) { $progress.html( progressString ); } + /** + * Performs the next export request. + * + * @since 4.9.6 + * + * @param {number} exporterIndex The 1-based index of the exporter to process, between 1 + * and exportersCount. + * @param {number} pageIndex The 1-based index of the page to process for the current exporter. + * @return {void} + */ function doNextExport( exporterIndex, pageIndex ) { $.ajax( { @@ -181,6 +244,13 @@ jQuery( function( $ ) { clearResultsAfterRow( $requestRow ); setErasureProgress( 0 ); + /** + * Handles a successful erasure. + * + * @since 4.9.6 + * + * @return {void} + */ function onErasureDoneSuccess() { var summaryMessage = __( 'No personal data was found for this user.' ), classes = 'notice-success'; @@ -207,6 +277,13 @@ jQuery( function( $ ) { setTimeout( function() { $rowActions.removeClass( 'processing' ); }, 500 ); } + /** + * Handles an erasure failure. + * + * @since 4.9.6 + * + * @return {void} + */ function onErasureFailure() { var summaryMessage = __( 'An error occurred while attempting to find and erase personal data.' ); @@ -217,6 +294,15 @@ jQuery( function( $ ) { setTimeout( function() { $rowActions.removeClass( 'processing' ); }, 500 ); } + /** + * Updates the erasure progress indicator based on the number of completed erasers. + * + * @since 5.4.0 + * + * @param {number} eraserIndex The number of erasers that have finished. Should be + * between 0 and erasersCount. + * @return {void} + */ function setErasureProgress( eraserIndex ) { var progress = ( erasersCount > 0 ? eraserIndex / erasersCount : 0 ), progressString = Math.round( progress * 100 ).toString() + '%'; @@ -224,6 +310,16 @@ jQuery( function( $ ) { $progress.html( progressString ); } + /** + * Performs the next erasure request. + * + * @since 4.9.6 + * + * @param {number} eraserIndex The 1-based index of the eraser to process, between 1 + * and erasersCount. + * @param {number} pageIndex The 1-based index of the page to process for the current eraser. + * @return {void} + */ function doNextErasure( eraserIndex, pageIndex ) { $.ajax({ url: window.ajaxurl, diff --git a/src/js/_enqueues/admin/site-health.js b/src/js/_enqueues/admin/site-health.js index 57d5c9cbcf289..8d8c40c70c3e9 100644 --- a/src/js/_enqueues/admin/site-health.js +++ b/src/js/_enqueues/admin/site-health.js @@ -1,11 +1,14 @@ /** - * Interactions used by the Site Health modules in WordPress. - * * @output wp-admin/js/site-health.js */ /* global ajaxurl, ClipboardJS, SiteHealth, wp */ +/** + * Handles the interactions used by the Site Health modules in WordPress. + * + * @param {JQueryStatic} $ The jQuery object. + */ jQuery( function( $ ) { var __ = wp.i18n.__, @@ -63,7 +66,7 @@ jQuery( function( $ ) { var hash = window.location.hash; if ( hash ) { - var requestedPanel = $( hash ); + var requestedPanel = $( document ).find( hash ); if ( requestedPanel.is( '.health-check-accordion-trigger' ) ) { requestedPanel.trigger( 'click' ); @@ -85,9 +88,9 @@ jQuery( function( $ ) { * * @since 5.6.0 * - * @param {Object} issue + * @param {Object} issue The issue data to validate. * - * @return {boolean} + * @return {boolean} True if the issue data is valid, false otherwise. */ function validateIssueData( issue ) { // Expected minimum format of a valid SiteHealth test response. @@ -137,6 +140,7 @@ jQuery( function( $ ) { * @since 5.2.0 * * @param {Object} issue The issue data. + * @return {void|boolean} True if the issue was appended, false otherwise. */ function appendIssue( issue ) { var template = wp.template( 'health-check-issue' ), @@ -346,6 +350,8 @@ jQuery( function( $ ) { /** * Add the details of a failed asynchronous test to the list of test results. * + * @param {string} url The URL of the failed test. + * @param {string} description The description of the failed test. * @since 5.6.0 */ function addFailedSiteHealthCheckNotice( url, description ) { @@ -390,6 +396,9 @@ jQuery( function( $ ) { } } + /** + * Get the sizes of the directories in the Site Health Info section. + */ function getDirectorySizes() { var timestamp = ( new Date().getTime() ); @@ -431,6 +440,11 @@ jQuery( function( $ ) { } ); } + /** + * Updates the directory sizes in the Site Health Info section. + * + * @param {Object} data The directory sizes data. + */ function updateDirSizes( data ) { var copyButton = $( 'button.button.copy-button' ); var clipboardText = copyButton.attr( 'data-clipboard-text' ); diff --git a/src/js/_enqueues/admin/site-icon.js b/src/js/_enqueues/admin/site-icon.js index a4f94797a5a48..59a098dc08aac 100644 --- a/src/js/_enqueues/admin/site-icon.js +++ b/src/js/_enqueues/admin/site-icon.js @@ -141,7 +141,7 @@ * * @since 6.5.0 * - * @param {array} attributes The attributes for the attachment. + * @param {Object} attributes The attributes for the attachment. */ function switchToUpdate( attributes ) { var i18nAppAlternativeString, i18nBrowserAlternativeString; diff --git a/src/js/_enqueues/admin/tags-box.js b/src/js/_enqueues/admin/tags-box.js index 99d664691c9c8..a3e1e901be22c 100644 --- a/src/js/_enqueues/admin/tags-box.js +++ b/src/js/_enqueues/admin/tags-box.js @@ -219,11 +219,11 @@ * * @memberOf tagBox * - * @param {Object} el The container HTML element. - * @param {Object|boolean} a When this is an HTML element the text of that - * element will be used for the new tag. - * @param {number|boolean} f If this value is not passed then the tag input - * field is focused. + * @param {Object} el The container HTML element. + * @param {Object|boolean} a When this is an HTML element the text of that + * element will be used for the new tag. + * @param {number|boolean} f If this value is not passed then the tag input + * field is focused. * * @return {boolean} Always returns false. */ @@ -287,8 +287,8 @@ * * @since 2.9.0 * - * @param {number|string} r The response message from the Ajax call. - * @param {string} stat The status of the Ajax request. + * @param {number|string} r The response message from the Ajax call. + * @param {string} stat The status of the Ajax request. * * @return {void} */ diff --git a/src/js/_enqueues/admin/tags-suggest.js b/src/js/_enqueues/admin/tags-suggest.js index d439f06ef0de8..e7469cfea2286 100644 --- a/src/js/_enqueues/admin/tags-suggest.js +++ b/src/js/_enqueues/admin/tags-suggest.js @@ -1,7 +1,11 @@ +/** + * @output wp-admin/js/tags-suggest.js + */ + /** * Default settings for jQuery UI Autocomplete for use with non-hierarchical taxonomies. * - * @output wp-admin/js/tags-suggest.js + * @param {JQueryStatic} $ The jQuery object. */ ( function( $ ) { var tempID = 0; @@ -10,16 +14,28 @@ _n = wp.i18n._n, sprintf = wp.i18n.sprintf; + /** + * Splits a string into an array of terms based on the separator. + * + * @param {string} val The string to split. + * @return {string[]} An array of terms. + */ function split( val ) { return val.split( new RegExp( separator + '\\s*' ) ); } + /** + * Gets the last term from a string based on the separator. + * + * @param {string} term The string to get the last term from. + * @return {string} The last term. + */ function getLast( term ) { return split( term ).pop(); } /** - * Add UI Autocomplete to an input or textarea element with presets for use + * Adds UI Autocomplete to an input or textarea element with presets for use * with non-hierarchical taxonomies. * * Example: `$( element ).wpTagsSuggest( options )`. diff --git a/src/js/_enqueues/admin/tags.js b/src/js/_enqueues/admin/tags.js index 88e38b6926309..8d449ca986863 100644 --- a/src/js/_enqueues/admin/tags.js +++ b/src/js/_enqueues/admin/tags.js @@ -9,6 +9,9 @@ /* global ajaxurl, wpAjax, showNotice, validateForm */ + /** + * @param {JQueryStatic} $ The jQuery object. + */ jQuery( function($) { var addingTerm = false; diff --git a/src/js/_enqueues/admin/user-profile.js b/src/js/_enqueues/admin/user-profile.js index 6a6be6745e357..23d10404129fe 100644 --- a/src/js/_enqueues/admin/user-profile.js +++ b/src/js/_enqueues/admin/user-profile.js @@ -1,8 +1,14 @@ +/* global ajaxurl, pwsL10n, userProfileL10n, ClipboardJS */ + /** * @output wp-admin/js/user-profile.js */ -/* global ajaxurl, pwsL10n, userProfileL10n, ClipboardJS */ +/** + * Handles the user profile functionality. + * + * @param {JQueryStatic} $ The jQuery object. + */ (function($) { var updateLock = false, isSubmitting = false, @@ -26,6 +32,17 @@ isSafari = window.safari !== 'undefined' && typeof window.safari === 'object', isFirefox = ua.indexOf( 'firefox' ) !== -1; + /** + * Generates a password and updates the password input field. + * + * If zxcvbn is not yet loaded, it will wait and try again. If the password + * input field is empty or the password wrapper is open, it will set the + * password to the generated value. If the user has already entered a + * password, it will check the strength of that password. It also binds + * the toggle button to show or hide the password and sets the initial state + * of the toggle button based on whether the password should be masked or + * not. Finally, it updates the label for confirming weak passwords. + */ function generatePassword() { if ( typeof zxcvbn !== 'function' ) { setTimeout( generatePassword, 50 ); @@ -61,6 +78,9 @@ $( '#pw-weak-text-label' ).text( __( 'Confirm use of weak password' ) ); } + /** + * Binds the password input field to update the current password and refresh the password strength area when the user types in the field. + */ function bindPass1() { currentPass = $pass1.val(); @@ -83,6 +103,11 @@ bindCapsLockWarning( $pass1 ); } + /** + * Resets the toggle button to show or hide the password. + * + * @param {boolean} show Whether to show the password or not. + */ function resetToggle( show ) { $toggleButton .attr({ @@ -96,6 +121,9 @@ .addClass( show ? 'dashicons-visibility' : 'dashicons-hidden' ); } + /** + * Binds the toggle button to show or hide the password. Also ensures that the password input type is set to password when the form is submitted. + */ function bindToggleButton() { if ( !! $toggleButton ) { // Do not rebind. @@ -158,10 +186,10 @@ /** * Helper function to insert an inline notice of success or failure. * - * @param {jQuery Object} $this The button element: the message will be inserted - * above this button - * @param {bool} success Whether the message is a success message. - * @param {string} message The message to insert. + * @param {jQuery} $this The button element: the message will be inserted + * above this button + * @param {boolean} success Whether the message is a success message. + * @param {string} message The message to insert. */ function addInlineNotice( $this, success, message ) { var resultDiv = $( '
', { @@ -187,6 +215,9 @@ $this.before( resultDiv ); } + /** + * Initializes the password form, including the password strength meter, weak password checkbox, and show/hide password toggle button. + */ function bindPasswordForm() { var $generateButton, $cancelButton; @@ -303,6 +334,9 @@ }); } + /** + * Sets CSS classes to the password strength results based on the password strength. + */ function check_pass_strength() { var pass1 = $('#pass1').val(), strength; @@ -400,7 +434,7 @@ * On macOS Safari and Firefox, the native warning is preferred, * so this function returns false to suppress custom warnings. * - * @param {KeyboardEvent} e The keydown event object. + * @param {KeyboardEvent} event The keydown event object. * * @return {boolean} True if Caps Lock is on, false otherwise. */ @@ -408,6 +442,9 @@ return event.getModifierState( 'CapsLock' ); } + /** + * Toggles the visibility of the weak password checkbox. + */ function showOrHideWeakPasswordCheckbox() { var passStrengthResult = $('#pass-strength-result'); @@ -456,10 +493,15 @@ } ); $( function() { - var $colorpicker, $stylesheet, user_id, current_user_id, + var $colorpicker, $stylesheet, $selectedMenuIconStyles, initialMenuIconStyles, user_id, current_user_id, select = $( '#display_name' ), - current_name = select.val(), - greeting = $( '#wp-admin-bar-my-account' ).find( '.display-name' ); + username = $( '#user_login' ).val(), + myAccount = $( '#wp-admin-bar-my-account' ), + displayNames = myAccount.find( '.user-display-name' ), + usernameLine = myAccount.find( '#wp-admin-bar-user-info .username' ), + menus = myAccount.children( '.ab-sub-wrapper' ).children( '[role="menu"]' ).filter( function() { + return this.getAttribute( 'aria-label' ) === displayNames.first().text(); + } ); $( '#pass1' ).val( '' ).on( 'input' + ' pwupdate', check_pass_strength ); $('#pass-strength-result').show(); @@ -503,21 +545,27 @@ }); /** - * Replaces "Howdy, *" in the admin toolbar whenever the display name dropdown is updated for one's own profile. + * Updates the display name in the admin toolbar whenever the display name dropdown is updated for one's own profile. */ select.on( 'change', function() { if ( user_id !== current_user_id ) { return; } - var display_name = this.value.trim() || current_name; + var display_name = this.value.trim() || username; + + displayNames.text( display_name ); + menus.attr( 'aria-label', display_name ); - greeting.text( display_name ); + // Only show the username when it differs from the display name. + usernameLine.prop( 'hidden', display_name === username ); } ); } $colorpicker = $( '#color-picker' ); $stylesheet = $( '#colors-css' ); + $selectedMenuIconStyles = $(); + initialMenuIconStyles = $colorpicker.find( '.selected .menu_icon_styles' ).val(); user_id = $( 'input#user_id' ).val(); current_user_id = $( 'input[name="checkuser_id"]' ).val(); @@ -553,6 +601,19 @@ } } + // Color the inline SVG menu icons. + if ( 0 === $selectedMenuIconStyles.length ) { + // Remove the color scheme styles that were already printed. + if ( initialMenuIconStyles ) { + $( 'style' ).each( function() { + this.textContent = this.textContent.replace( initialMenuIconStyles, '' ); + } ); + } + // Insert before the colors stylesheet, so that the stylesheet can override these styles. + $selectedMenuIconStyles = $( ' +
+

+ 'error' ) + ); + ?> +
+
- + + + + +
+

+ 'error' ) + ); + ?> +
$preload_paths Paths to preload. + */ + $preload_paths = apply_filters( 'wp_options_connectors_wp_admin_preload_paths', $preload_paths ); + // Use rest_preload_api_request to gather the preloaded data $preload_data = array_reduce( $preload_paths, @@ -146,8 +158,12 @@ function wp_options_connectors_wp_admin_enqueue_scripts( $hook_suffix ) { // Get all registered routes $routes = wp_get_options_connectors_wp_admin_routes(); - // Get boot module asset file for dependencies + // Get boot module asset file for dependencies. Plugins that build their own + // boot module use it; everyone else falls back to the copy bundled with Core. $asset_file = ABSPATH . WPINC . '/js/dist/script-modules/boot/index.min.asset.php'; + if ( ! file_exists( $asset_file ) ) { + $asset_file = ABSPATH . WPINC . '/js/dist/script-modules/boot/index.min.asset.php'; + } if ( file_exists( $asset_file ) ) { $asset = require $asset_file; @@ -283,7 +299,7 @@ function wp_options_connectors_wp_admin_render_page() { } /* Hide legacy admin elements */ - body.js #wpbody-content > div:not(.boot-layout-container):not(#screen-meta) { + body.js #wpbody-content > div:not(#options-connectors-wp-admin-app):not(#screen-meta) { display: none; } body.js #wpfooter { @@ -315,10 +331,23 @@ function wp_options_connectors_wp_admin_render_page() { } } +
+

+ 'error' ) + ); + ?> +
+
- + + + + +
+

+ 'error' ) + ); + ?> +
fn() -); -function useStableCallback(callback) { - const stable = useRefWithInit(createStableCallback).current; - stable.next = callback; - useSafeInsertionEffect(stable.effect); - return stable.trampoline; -} -function createStableCallback() { - const stable = { - next: void 0, - callback: assertNotCalled, - trampoline: (...args) => stable.callback?.(...args), - effect: () => { - stable.callback = stable.next; - } - }; - return stable; -} -function assertNotCalled() { - if (true) { - throw ( - /* minify-error-disabled */ - new Error("Base UI: Cannot call an event handler while rendering.") - ); - } -} - -// node_modules/@base-ui/utils/useIsoLayoutEffect.mjs -var React4 = __toESM(require_react(), 1); -var noop = () => { -}; -var useIsoLayoutEffect = typeof document !== "undefined" ? React4.useLayoutEffect : noop; - -// node_modules/@base-ui/utils/warn.mjs -var set2; -if (true) { - set2 = /* @__PURE__ */ new Set(); -} -function warn(...messages) { - if (true) { - const messageKey = messages.join(" "); - if (!set2.has(messageKey)) { - set2.add(messageKey); - console.warn(`Base UI: ${messageKey}`); - } - } -} - -// node_modules/@base-ui/react/internals/direction-context/DirectionContext.mjs -var React5 = __toESM(require_react(), 1); -var DirectionContext = /* @__PURE__ */ React5.createContext(void 0); -if (true) DirectionContext.displayName = "DirectionContext"; -function useDirection() { - const context = React5.useContext(DirectionContext); - return context?.direction ?? "ltr"; -} - -// node_modules/@base-ui/react/internals/useRenderElement.mjs -var React8 = __toESM(require_react(), 1); - -// node_modules/@base-ui/utils/useMergedRefs.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/useMergedRefs.mjs function useMergedRefs(a, b, c, d) { const forkRef = useRefWithInit(createForkRef).current; if (didChange(forkRef, a, b, c, d)) { @@ -464,7 +361,7 @@ function update(forkRef, refs) { if (typeof cleanupCallback === "function") { cleanupCallback(); } else { - ref(null); + void ref(null); } break; } @@ -480,19 +377,19 @@ function update(forkRef, refs) { }; } -// node_modules/@base-ui/utils/getReactElementRef.mjs -var React7 = __toESM(require_react(), 1); +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/getReactElementRef.mjs +var React3 = __toESM(require_react(), 1); -// node_modules/@base-ui/utils/reactVersion.mjs -var React6 = __toESM(require_react(), 1); -var majorVersion = parseInt(React6.version, 10); +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/reactVersion.mjs +var React2 = __toESM(require_react(), 1); +var majorVersion = parseInt(React2.version, 10); function isReactVersionAtLeast(reactVersionToCheck) { return majorVersion >= reactVersionToCheck; } -// node_modules/@base-ui/utils/getReactElementRef.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/getReactElementRef.mjs function getReactElementRef(element) { - if (!/* @__PURE__ */ React7.isValidElement(element)) { + if (!/* @__PURE__ */ React3.isValidElement(element)) { return null; } const reactElement = element; @@ -500,7 +397,7 @@ function getReactElementRef(element) { return (isReactVersionAtLeast(19) ? propsWithRef?.ref : reactElement.ref) ?? null; } -// node_modules/@base-ui/utils/mergeObjects.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/mergeObjects.mjs function mergeObjects(a, b) { if (a && !b) { return a; @@ -517,13 +414,39 @@ function mergeObjects(a, b) { return void 0; } -// node_modules/@base-ui/utils/empty.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/createLogOnce.mjs +var loggedMessages; +if (true) { + loggedMessages = /* @__PURE__ */ new Set(); +} +function createLogOnce(severity, prefix) { + return function logOnce(...messages) { + if (true) { + const message = messages.join(" "); + const output = prefix ? `${prefix}: ${message}` : message; + const key = `${severity}:${output}`; + if (!loggedMessages.has(key)) { + loggedMessages.add(key); + if (severity === "warn") { + console.warn(output); + } else { + console.error(output); + } + } + } + }; +} + +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/warn.mjs +var warn = createLogOnce("warn", "Base UI"); + +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/empty.mjs function NOOP() { } var EMPTY_ARRAY = Object.freeze([]); var EMPTY_OBJECT = Object.freeze({}); -// node_modules/@base-ui/react/internals/getStateAttributesProps.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/getStateAttributesProps.mjs function getStateAttributesProps(state, customMapping) { const props = {}; for (const key in state) { @@ -544,17 +467,17 @@ function getStateAttributesProps(state, customMapping) { return props; } -// node_modules/@base-ui/react/utils/resolveClassName.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/utils/resolveClassName.mjs function resolveClassName(className, state) { return typeof className === "function" ? className(state) : className; } -// node_modules/@base-ui/react/utils/resolveStyle.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/utils/resolveStyle.mjs function resolveStyle(style, state) { return typeof style === "function" ? style(state) : style; } -// node_modules/@base-ui/react/merge-props/mergeProps.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/merge-props/mergeProps.mjs var EMPTY_PROPS = {}; function mergeProps(a, b, c, d, e) { if (!c && !d && !e && !a) { @@ -591,14 +514,14 @@ function mergePropsN(props) { function createInitialMergedProps(inputProps) { if (isPropsGetter(inputProps)) { return { - ...resolvePropsGetter(inputProps, EMPTY_PROPS) + ...inputProps(EMPTY_PROPS) }; } return copyInitialProps(inputProps); } function mergeInto(merged, inputProps) { if (isPropsGetter(inputProps)) { - return resolvePropsGetter(inputProps, merged); + return inputProps(merged); } return mutablyMergeInto(merged, inputProps); } @@ -649,12 +572,6 @@ function isEventHandler(key, value) { function isPropsGetter(inputProps) { return typeof inputProps === "function"; } -function resolvePropsGetter(inputProps, previousProps) { - if (isPropsGetter(inputProps)) { - return inputProps(previousProps); - } - return inputProps ?? EMPTY_PROPS; -} function mergeEventHandlers(ourHandler, theirHandler) { if (!theirHandler) { return ourHandler; @@ -709,38 +626,40 @@ function isSyntheticEvent(event) { return event != null && typeof event === "object" && "nativeEvent" in event; } -// node_modules/@base-ui/react/internals/useRenderElement.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/useRenderElement.mjs var import_react = __toESM(require_react(), 1); function useRenderElement(element, componentProps, params = {}) { - const renderProp = componentProps.render; - const outProps = useRenderElementProps(componentProps, params); + let renderProp = componentProps.render; + if (params.enabled !== false) { + renderProp = unwrapLazyRenderProp(renderProp); + } + const outProps = useRenderElementProps(componentProps, params, renderProp); if (params.enabled === false) { return null; } const state = params.state ?? EMPTY_OBJECT; return evaluateRenderProp(element, renderProp, outProps, state); } -function useRenderElementProps(componentProps, params = {}) { +function useRenderElementProps(componentProps, params, renderProp) { const { className: classNameProp, - style: styleProp, - render: renderProp + style: styleProp } = componentProps; const { state = EMPTY_OBJECT, ref, props, - stateAttributesMapping: stateAttributesMapping3, + stateAttributesMapping, enabled = true } = params; const className = enabled ? resolveClassName(classNameProp, state) : void 0; const style = enabled ? resolveStyle(styleProp, state) : void 0; - const stateProps = enabled ? getStateAttributesProps(state, stateAttributesMapping3) : EMPTY_OBJECT; + const stateProps = enabled ? getStateAttributesProps(state, stateAttributesMapping) : EMPTY_OBJECT; const resolvedProps = enabled && props ? resolveRenderFunctionProps(props) : void 0; const outProps = enabled ? mergeObjects(stateProps, resolvedProps) ?? {} : EMPTY_OBJECT; if (typeof document !== "undefined") { if (!enabled) { - useMergedRefs(null, null); + void useMergedRefs(null, null); } else if (Array.isArray(ref)) { outProps.ref = useMergedRefsN([outProps.ref, getReactElementRef(renderProp), ...ref]); } else { @@ -767,6 +686,13 @@ function resolveRenderFunctionProps(props) { var REACT_LAZY_TYPE = /* @__PURE__ */ Symbol.for("react.lazy"); var COMPONENT_IDENTIFIER_PATTERN = /^[A-Z][A-Za-z0-9$]*$/; var LOWERCASE_CHARACTER_PATTERN = /[a-z]/; +function unwrapLazyRenderProp(render) { + if (render?.$$typeof !== REACT_LAZY_TYPE) { + return render; + } + const unwrapped = React4.Children.toArray(render)[0]; + return /* @__PURE__ */ React4.isValidElement(unwrapped) ? unwrapped : render; +} function evaluateRenderProp(element, render, props, state) { if (render) { if (typeof render === "function") { @@ -777,24 +703,14 @@ function evaluateRenderProp(element, render, props, state) { } const mergedProps = mergeProps(props, render.props); mergedProps.ref = props.ref; - let newElement = render; - if (newElement?.$$typeof === REACT_LAZY_TYPE) { - const children = React8.Children.toArray(render); - newElement = children[0]; - } if (true) { - if (!/* @__PURE__ */ React8.isValidElement(newElement)) { + if (!/* @__PURE__ */ React4.isValidElement(render)) { throw new Error(["Base UI: The `render` prop was provided an invalid React element as `React.isValidElement(render)` is `false`.", "A valid React element must be provided to the `render` prop because it is cloned with props to replace the default element.", "https://base-ui.com/r/invalid-render-prop"].join("\n")); } } - return /* @__PURE__ */ React8.cloneElement(newElement, mergedProps); - } - if (element) { - if (typeof element === "string") { - return renderTag(element, props); - } + return /* @__PURE__ */ React4.cloneElement(render, mergedProps); } - throw new Error(true ? "Base UI: Render element or function are not defined." : formatErrorMessage_default(8)); + return renderTag(element, props); } function warnIfRenderPropLooksLikeComponent(renderFn) { const functionName = renderFn.name; @@ -824,150 +740,68 @@ function renderTag(Tag, props) { key: props.key }); } - return /* @__PURE__ */ React8.createElement(Tag, props); + return /* @__PURE__ */ React4.createElement(Tag, props); } -// node_modules/@base-ui/utils/useId.mjs -var React9 = __toESM(require_react(), 1); -var globalId = 0; -function useGlobalId(idOverride, prefix = "mui") { - const [defaultId, setDefaultId] = React9.useState(idOverride); - const id = idOverride || defaultId; - React9.useEffect(() => { - if (defaultId == null) { - globalId += 1; - setDefaultId(`${prefix}-${globalId}`); +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/safeReact.mjs +var React5 = __toESM(require_react(), 1); +var SafeReact = { + ...React5 +}; + +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/useStableCallback.mjs +var useInsertionEffect = SafeReact.useInsertionEffect; +var useSafeInsertionEffect = ( + // React 17 doesn't have useInsertionEffect. + useInsertionEffect && // Preact replaces useInsertionEffect with useLayoutEffect and fires too late. + useInsertionEffect !== SafeReact.useLayoutEffect ? useInsertionEffect : (fn) => fn() +); +function useStableCallback(callback) { + const stable = useRefWithInit(createStableCallback).current; + stable.next = callback; + useSafeInsertionEffect(stable.effect); + return stable.trampoline; +} +function createStableCallback() { + const stable = { + next: void 0, + callback: assertNotCalled, + trampoline: (...args) => stable.callback?.(...args), + effect: () => { + stable.callback = stable.next; } - }, [defaultId, prefix]); - return id; + }; + return stable; } -var maybeReactUseId = SafeReact.useId; -function useId(idOverride, prefix) { - if (maybeReactUseId !== void 0) { - const reactId = maybeReactUseId(); - return idOverride ?? (prefix ? `${prefix}-${reactId}` : reactId); +function assertNotCalled() { + if (true) { + throw ( + /* minify-error-disabled */ + new Error("Base UI: Cannot call an event handler while rendering.") + ); } - return useGlobalId(idOverride, prefix); } -// node_modules/@base-ui/react/internals/useBaseUiId.mjs -function useBaseUiId(idOverride) { - return useId(idOverride, "base-ui"); -} +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/useIsoLayoutEffect.mjs +var React6 = __toESM(require_react(), 1); +var noop = () => { +}; +var useIsoLayoutEffect = typeof document !== "undefined" ? React6.useLayoutEffect : noop; -// node_modules/@base-ui/react/internals/reason-parts.mjs -var reason_parts_exports = {}; -__export(reason_parts_exports, { - cancelOpen: () => cancelOpen, - chipRemovePress: () => chipRemovePress, - clearPress: () => clearPress, - closePress: () => closePress, - closeWatcher: () => closeWatcher, - decrementPress: () => decrementPress, - disabled: () => disabled, - drag: () => drag, - escapeKey: () => escapeKey, - focusOut: () => focusOut, - imperativeAction: () => imperativeAction, - incrementPress: () => incrementPress, - initial: () => initial, - inputBlur: () => inputBlur, - inputChange: () => inputChange, - inputClear: () => inputClear, - inputPaste: () => inputPaste, - inputPress: () => inputPress, - itemPress: () => itemPress, - keyboard: () => keyboard, - linkPress: () => linkPress, - listNavigation: () => listNavigation, - missing: () => missing, - none: () => none, - outsidePress: () => outsidePress, - pointer: () => pointer, - scrub: () => scrub, - siblingOpen: () => siblingOpen, - swipe: () => swipe, - trackPress: () => trackPress, - triggerFocus: () => triggerFocus, - triggerHover: () => triggerHover, - triggerPress: () => triggerPress, - wheel: () => wheel, - windowResize: () => windowResize -}); -var none = "none"; -var triggerPress = "trigger-press"; -var triggerHover = "trigger-hover"; -var triggerFocus = "trigger-focus"; -var outsidePress = "outside-press"; -var itemPress = "item-press"; -var closePress = "close-press"; -var linkPress = "link-press"; -var clearPress = "clear-press"; -var chipRemovePress = "chip-remove-press"; -var trackPress = "track-press"; -var incrementPress = "increment-press"; -var decrementPress = "decrement-press"; -var inputChange = "input-change"; -var inputClear = "input-clear"; -var inputBlur = "input-blur"; -var inputPaste = "input-paste"; -var inputPress = "input-press"; -var focusOut = "focus-out"; -var escapeKey = "escape-key"; -var closeWatcher = "close-watcher"; -var listNavigation = "list-navigation"; -var keyboard = "keyboard"; -var pointer = "pointer"; -var drag = "drag"; -var wheel = "wheel"; -var scrub = "scrub"; -var cancelOpen = "cancel-open"; -var siblingOpen = "sibling-open"; -var disabled = "disabled"; -var missing = "missing"; -var initial = "initial"; -var imperativeAction = "imperative-action"; -var swipe = "swipe"; -var windowResize = "window-resize"; +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/useOpenChangeComplete.mjs +var React8 = __toESM(require_react(), 1); -// node_modules/@base-ui/react/internals/createBaseUIEventDetails.mjs -function createChangeEventDetails(reason, event, trigger, customProperties) { - let canceled = false; - let allowPropagation = false; - const custom = customProperties ?? EMPTY_OBJECT; - const details = { - reason, - event: event ?? new Event("base-ui"), - cancel() { - canceled = true; - }, - allowPropagation() { - allowPropagation = true; - }, - get isCanceled() { - return canceled; - }, - get isPropagationAllowed() { - return allowPropagation; - }, - trigger, - ...custom - }; - return details; -} - -// node_modules/@base-ui/react/internals/useTransitionStatus.mjs -var React11 = __toESM(require_react(), 1); +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/useAnimationsFinished.mjs +var ReactDOM = __toESM(require_react_dom(), 1); -// node_modules/@base-ui/utils/useOnMount.mjs -var React10 = __toESM(require_react(), 1); -var EMPTY = []; +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/useOnMount.mjs +var React7 = __toESM(require_react(), 1); function useOnMount(fn) { - React10.useEffect(fn, EMPTY); + React7.useEffect(fn, EMPTY_ARRAY); } -// node_modules/@base-ui/utils/useAnimationFrame.mjs -var EMPTY2 = null; +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/useAnimationFrame.mjs +var EMPTY = null; var LAST_RAF = globalThis.requestAnimationFrame; var Scheduler = class { /* This implementation uses an array as a backing data-structure for frame callbacks. @@ -1012,6 +846,9 @@ var Scheduler = class { if (index2 < 0 || index2 >= this.callbacks.length) { return; } + if (this.callbacks[index2] === null) { + return; + } this.callbacks[index2] = null; this.callbacksCount -= 1; } @@ -1027,21 +864,21 @@ var AnimationFrame = class _AnimationFrame { static cancel(id) { return scheduler.cancel(id); } - currentId = EMPTY2; + currentId = EMPTY; /** * Executes `fn` after `delay`, clearing any previously scheduled call. */ request(fn) { this.cancel(); this.currentId = scheduler.request(() => { - this.currentId = EMPTY2; + this.currentId = EMPTY; fn(); }); } cancel = () => { - if (this.currentId !== EMPTY2) { + if (this.currentId !== EMPTY) { scheduler.cancel(this.currentId); - this.currentId = EMPTY2; + this.currentId = EMPTY; } }; disposeEffect = () => { @@ -1054,22 +891,168 @@ function useAnimationFrame() { return timeout; } -// node_modules/@base-ui/react/internals/useTransitionStatus.mjs -function useTransitionStatus(open, enableIdleState = false, deferEndingState = false) { - const [transitionStatus, setTransitionStatus] = React11.useState(open && enableIdleState ? "idle" : void 0); - const [mounted, setMounted] = React11.useState(open); - if (open && !mounted) { +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/utils/resolveRef.mjs +function resolveRef(maybeRef) { + if (maybeRef == null) { + return maybeRef; + } + return "current" in maybeRef ? maybeRef.current : maybeRef; +} + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/utils/getFiniteAnimations.mjs +function getFiniteAnimations(element, options) { + return element.getAnimations(options).filter((animation) => { + const timing = animation.effect?.getTiming(); + return timing?.duration !== Infinity && timing?.iterations !== Infinity; + }); +} + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/TransitionStatusDataAttributes.mjs +var startingStyle = "data-starting-style"; +var endingStyle = "data-ending-style"; + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/useAnimationsFinished.mjs +var pendingCallbacks = null; +function flushBeforePaint(fn) { + if (!pendingCallbacks) { + const callbacks = []; + pendingCallbacks = callbacks; + queueMicrotask(() => { + pendingCallbacks = null; + ReactDOM.flushSync(() => { + for (const callback of callbacks) { + callback(); + } + }); + }); + } + pendingCallbacks.push(fn); +} +function useAnimationsFinished(elementOrRef, waitForStartingStyleRemoved = false, batch = false) { + const frame = useAnimationFrame(); + return useStableCallback((fnToExecute, signal = null) => { + frame.cancel(); + const element = resolveRef(elementOrRef); + if (element == null) { + return; + } + const resolvedElement = element; + const done = () => { + if (!batch) { + ReactDOM.flushSync(fnToExecute); + return; + } + flushBeforePaint(() => { + if (!signal?.aborted) { + fnToExecute(); + } + }); + }; + if (typeof resolvedElement.getAnimations !== "function" || globalThis.BASE_UI_ANIMATIONS_DISABLED) { + fnToExecute(); + return; + } + function exec() { + Promise.all(getFiniteAnimations(resolvedElement).map((animation) => animation.finished.then(NOOP))).then(() => { + if (!signal?.aborted) { + done(); + } + }, () => { + if (signal?.aborted) { + return; + } + const currentAnimations = getFiniteAnimations(resolvedElement); + if (currentAnimations.some((animation) => animation.pending || animation.playState !== "finished")) { + exec(); + return; + } + done(); + }); + } + if (waitForStartingStyleRemoved) { + const startingStyleAttribute = startingStyle; + if (!resolvedElement.hasAttribute(startingStyleAttribute)) { + frame.request(exec); + return; + } + const attributeObserver = new MutationObserver(() => { + if (!resolvedElement.hasAttribute(startingStyleAttribute)) { + attributeObserver.disconnect(); + exec(); + } + }); + attributeObserver.observe(resolvedElement, { + attributes: true, + attributeFilter: [startingStyleAttribute] + }); + signal?.addEventListener("abort", () => attributeObserver.disconnect(), { + once: true + }); + return; + } + frame.request(exec); + }); +} + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/useOpenChangeComplete.mjs +function useOpenChangeComplete(parameters) { + const { + enabled = true, + open: open2, + ref, + batch = false, + onComplete: onCompleteParam + } = parameters; + const onComplete = useStableCallback(onCompleteParam); + const runOnceAnimationsFinish = useAnimationsFinished(ref, open2, batch); + React8.useEffect(() => { + if (!enabled) { + return void 0; + } + const abortController = new AbortController(); + runOnceAnimationsFinish(onComplete, abortController.signal); + return () => { + abortController.abort(); + }; + }, [enabled, open2, onComplete, runOnceAnimationsFinish]); +} + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/stateAttributesMapping.mjs +var STARTING_HOOK = { + [startingStyle]: "" +}; +var ENDING_HOOK = { + [endingStyle]: "" +}; +var transitionStatusMapping = { + transitionStatus(value) { + if (value === "starting") { + return STARTING_HOOK; + } + if (value === "ending") { + return ENDING_HOOK; + } + return null; + } +}; + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/useTransitionStatus.mjs +var React9 = __toESM(require_react(), 1); +function useTransitionStatus(open2, enableIdleState = false, deferEndingState = false, animateInitialOpen = false) { + const [transitionStatus, setTransitionStatus] = React9.useState(open2 && enableIdleState ? "idle" : void 0); + const [mounted, setMounted] = React9.useState(open2 && !animateInitialOpen); + if (open2 && !mounted) { setMounted(true); setTransitionStatus("starting"); } - if (!open && mounted && transitionStatus !== "ending" && !deferEndingState) { + if (!open2 && mounted && transitionStatus !== "ending" && !deferEndingState) { setTransitionStatus("ending"); } - if (!open && !mounted && transitionStatus === "ending") { + if (!open2 && !mounted && transitionStatus === "ending") { setTransitionStatus(void 0); } useIsoLayoutEffect(() => { - if (!open && mounted && transitionStatus !== "ending" && deferEndingState) { + if (!open2 && mounted && transitionStatus !== "ending" && deferEndingState) { const frame = AnimationFrame.request(() => { setTransitionStatus("ending"); }); @@ -1078,9 +1061,9 @@ function useTransitionStatus(open, enableIdleState = false, deferEndingState = f }; } return void 0; - }, [open, mounted, transitionStatus, deferEndingState]); + }, [open2, mounted, transitionStatus, deferEndingState]); useIsoLayoutEffect(() => { - if (!open || enableIdleState) { + if (!open2 || enableIdleState || transitionStatus === void 0) { return void 0; } const frame = AnimationFrame.request(() => { @@ -1089,12 +1072,12 @@ function useTransitionStatus(open, enableIdleState = false, deferEndingState = f return () => { AnimationFrame.cancel(frame); }; - }, [enableIdleState, open]); + }, [enableIdleState, open2, transitionStatus]); useIsoLayoutEffect(() => { - if (!open || !enableIdleState) { + if (!open2 || !enableIdleState) { return void 0; } - if (open && mounted && transitionStatus !== "idle") { + if (open2 && mounted && transitionStatus !== "idle") { setTransitionStatus("starting"); } const frame = AnimationFrame.request(() => { @@ -1103,7 +1086,7 @@ function useTransitionStatus(open, enableIdleState = false, deferEndingState = f return () => { AnimationFrame.cancel(frame); }; - }, [enableIdleState, open, mounted, transitionStatus]); + }, [enableIdleState, open2, mounted, transitionStatus]); return { mounted, setMounted, @@ -1111,74 +1094,272 @@ function useTransitionStatus(open, enableIdleState = false, deferEndingState = f }; } -// node_modules/@base-ui/react/internals/stateAttributesMapping.mjs -var TransitionStatusDataAttributes = /* @__PURE__ */ (function(TransitionStatusDataAttributes2) { - TransitionStatusDataAttributes2["startingStyle"] = "data-starting-style"; - TransitionStatusDataAttributes2["endingStyle"] = "data-ending-style"; - return TransitionStatusDataAttributes2; -})({}); -var STARTING_HOOK = { - [TransitionStatusDataAttributes.startingStyle]: "" -}; -var ENDING_HOOK = { - [TransitionStatusDataAttributes.endingStyle]: "" -}; -var transitionStatusMapping = { - transitionStatus(value) { - if (value === "starting") { - return STARTING_HOOK; - } - if (value === "ending") { - return ENDING_HOOK; - } - return null; +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/useTimeout.mjs +var EMPTY2 = 0; +var Timeout = class _Timeout { + static create() { + return new _Timeout(); } -}; - -// node_modules/@base-ui/react/internals/use-button/useButton.mjs -var React14 = __toESM(require_react(), 1); - -// node_modules/@floating-ui/utils/dist/floating-ui.utils.dom.mjs -function hasWindow() { - return typeof window !== "undefined"; -} -function getNodeName(node) { - if (isNode(node)) { - return (node.nodeName || "").toLowerCase(); + currentId = EMPTY2; + /** + * Executes `fn` after `delay`, clearing any previously scheduled call. + */ + start(delay, fn) { + this.clear(); + this.currentId = setTimeout(() => { + this.currentId = EMPTY2; + fn(); + }, delay); } - return "#document"; + isStarted() { + return this.currentId !== EMPTY2; + } + clear = () => { + if (this.currentId !== EMPTY2) { + clearTimeout(this.currentId); + this.currentId = EMPTY2; + } + }; + disposeEffect = () => { + return this.clear; + }; +}; +function useTimeout() { + const timeout = useRefWithInit(Timeout.create).current; + useOnMount(timeout.disposeEffect); + return timeout; } -function getWindow(node) { - var _node$ownerDocument; - return (node == null || (_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.defaultView) || window; + +// node_modules/.store/clsx@2.1.1-XOLP1vUL9vEGfpiXm37fAw/node_modules/clsx/dist/clsx.mjs +function r(e) { + var t, f, n = ""; + if ("string" == typeof e || "number" == typeof e) n += e; + else if ("object" == typeof e) if (Array.isArray(e)) { + var o = e.length; + for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f); + } else for (f in e) e[f] && (n && (n += " "), n += f); + return n; } -function getDocumentElement(node) { - var _ref; - return (_ref = (isNode(node) ? node.ownerDocument : node.document) || window.document) == null ? void 0 : _ref.documentElement; +function clsx() { + for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t); + return n; } -function isNode(value) { - if (!hasWindow()) { - return false; +var clsx_default = clsx; + +// packages/ui/build-module/icon/icon.mjs +var import_element = __toESM(require_element(), 1); +var import_primitives = __toESM(require_primitives(), 1); +var import_jsx_runtime = __toESM(require_jsx_runtime(), 1); +var Icon = (0, import_element.forwardRef)( + function UnforwardedIcon({ icon, size: size4 = 24, style, ...restProps }, ref) { + const mergedStyle = icon.props.style || style ? { ...icon.props.style, ...style } : void 0; + return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( + import_primitives.SVG, + { + ref, + ...icon.props, + ...restProps, + ...mergedStyle ? { style: mergedStyle } : {}, + width: size4, + height: size4 + } + ); } - return value instanceof Node || value instanceof getWindow(value).Node; +); + +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/error.mjs +var error = createLogOnce("error", "Base UI"); + +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/useId.mjs +var React11 = __toESM(require_react(), 1); + +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/useForcedRerendering.mjs +var React10 = __toESM(require_react(), 1); +function useForcedRerendering() { + const [, setState] = React10.useState({}); + return React10.useCallback(() => { + setState({}); + }, []); } -function isElement(value) { - if (!hasWindow()) { - return false; - } - return value instanceof Element || value instanceof getWindow(value).Element; + +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/useId.mjs +var globalId = 0; +function useGlobalId(idOverride, prefix = "mui") { + const defaultId = React11.useRef(void 0); + const forceRerender = useForcedRerendering(); + const id = idOverride ?? defaultId.current; + React11.useEffect(() => { + if (defaultId.current == null) { + globalId += 1; + defaultId.current = `${prefix}-${globalId}`; + if (idOverride == null) { + forceRerender(); + } + } + }, [idOverride, prefix, forceRerender]); + return id; } -function isHTMLElement(value) { - if (!hasWindow()) { - return false; +var maybeReactUseId = SafeReact.useId; +function useId(idOverride, prefix) { + if (maybeReactUseId !== void 0) { + const reactId = maybeReactUseId(); + return idOverride ?? (prefix ? `${prefix}-${reactId}` : reactId); } - return value instanceof HTMLElement || value instanceof getWindow(value).HTMLElement; + return useGlobalId(idOverride, prefix); } -function isShadowRoot(value) { - if (!hasWindow() || typeof ShadowRoot === "undefined") { - return false; - } - return value instanceof ShadowRoot || value instanceof getWindow(value).ShadowRoot; + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/useBaseUiId.mjs +function useBaseUiId(idOverride) { + return useId(idOverride, "base-ui"); +} + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/createBaseUIEventDetails.mjs +function createChangeEventDetails(reason, event, trigger, customProperties) { + let canceled = false; + let allowPropagation = false; + const custom = customProperties ?? EMPTY_OBJECT; + const details = { + reason, + event: event ?? new Event("base-ui"), + cancel() { + canceled = true; + }, + allowPropagation() { + allowPropagation = true; + }, + get isCanceled() { + return canceled; + }, + get isPropagationAllowed() { + return allowPropagation; + }, + trigger, + ...custom + }; + return details; +} + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/reason-parts.mjs +var reason_parts_exports = {}; +__export(reason_parts_exports, { + cancelOpen: () => cancelOpen, + chipRemovePress: () => chipRemovePress, + clearPress: () => clearPress, + closePress: () => closePress, + closeWatcher: () => closeWatcher, + decrementPress: () => decrementPress, + disabled: () => disabled, + drag: () => drag, + escapeKey: () => escapeKey, + focusOut: () => focusOut, + imperativeAction: () => imperativeAction, + incrementPress: () => incrementPress, + initial: () => initial, + inputBlur: () => inputBlur, + inputChange: () => inputChange, + inputClear: () => inputClear, + inputPaste: () => inputPaste, + inputPress: () => inputPress, + itemPress: () => itemPress, + keyboard: () => keyboard, + linkPress: () => linkPress, + listNavigation: () => listNavigation, + missing: () => missing, + none: () => none, + outsidePress: () => outsidePress, + pointer: () => pointer, + popupClose: () => popupClose, + scrub: () => scrub, + siblingOpen: () => siblingOpen, + swipe: () => swipe, + trackPress: () => trackPress, + triggerFocus: () => triggerFocus, + triggerHover: () => triggerHover, + triggerPress: () => triggerPress, + wheel: () => wheel, + windowResize: () => windowResize +}); +var none = "none"; +var triggerPress = "trigger-press"; +var triggerHover = "trigger-hover"; +var triggerFocus = "trigger-focus"; +var outsidePress = "outside-press"; +var itemPress = "item-press"; +var closePress = "close-press"; +var linkPress = "link-press"; +var clearPress = "clear-press"; +var chipRemovePress = "chip-remove-press"; +var trackPress = "track-press"; +var incrementPress = "increment-press"; +var decrementPress = "decrement-press"; +var inputChange = "input-change"; +var inputClear = "input-clear"; +var inputBlur = "input-blur"; +var inputPaste = "input-paste"; +var inputPress = "input-press"; +var focusOut = "focus-out"; +var escapeKey = "escape-key"; +var closeWatcher = "close-watcher"; +var listNavigation = "list-navigation"; +var keyboard = "keyboard"; +var pointer = "pointer"; +var drag = "drag"; +var wheel = "wheel"; +var scrub = "scrub"; +var popupClose = "popup-close"; +var cancelOpen = "cancel-open"; +var siblingOpen = "sibling-open"; +var disabled = "disabled"; +var missing = "missing"; +var initial = "initial"; +var imperativeAction = "imperative-action"; +var swipe = "swipe"; +var windowResize = "window-resize"; + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/use-button/useButton.mjs +var React14 = __toESM(require_react(), 1); + +// node_modules/.store/@floating-ui/utils@0.2.12-gOxTzMf36nCrbeHpEew-rw/node_modules/@floating-ui/utils/dist/floating-ui.utils.dom.mjs +function hasWindow() { + return typeof window !== "undefined"; +} +function getNodeName(node) { + if (isNode(node)) { + return (node.nodeName || "").toLowerCase(); + } + return "#document"; +} +function getWindow(node) { + var _node$ownerDocument; + return (node == null || (_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.defaultView) || window; +} +function getDocumentElement(node) { + var _ref; + return (_ref = (isNode(node) ? node.ownerDocument : node.document) || window.document) == null ? void 0 : _ref.documentElement; +} +function isNode(value) { + if (!hasWindow()) { + return false; + } + return value instanceof Node || value instanceof getWindow(value).Node; +} +function isElement(value) { + if (!hasWindow()) { + return false; + } + return value instanceof Element || value instanceof getWindow(value).Element; +} +function isHTMLElement(value) { + if (!hasWindow()) { + return false; + } + return value instanceof HTMLElement || value instanceof getWindow(value).HTMLElement; +} +function isShadowRoot(value) { + if (!hasWindow() || typeof ShadowRoot === "undefined") { + return false; + } + return value instanceof ShadowRoot || value instanceof getWindow(value).ShadowRoot; } function isOverflowElement(element) { const { @@ -1265,7 +1446,7 @@ function getParentNode(node) { function getNearestOverflowAncestor(node) { const parentNode = getParentNode(node); if (isLastTraversableNode(parentNode)) { - return node.ownerDocument ? node.ownerDocument.body : node.body; + return (node.ownerDocument || node).body; } if (isHTMLElement(parentNode) && isOverflowElement(parentNode)) { return parentNode; @@ -1294,7 +1475,7 @@ function getFrameElement(win) { return win.parent && Object.getPrototypeOf(win.parent) ? win.frameElement : null; } -// node_modules/@base-ui/react/internals/composite/root/CompositeRootContext.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/composite/root/CompositeRootContext.mjs var React12 = __toESM(require_react(), 1); var CompositeRootContext = /* @__PURE__ */ React12.createContext(void 0); if (true) CompositeRootContext.displayName = "CompositeRootContext"; @@ -1306,7 +1487,7 @@ function useCompositeRootContext(optional = false) { return context; } -// node_modules/@base-ui/react/utils/useFocusableWhenDisabled.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/utils/useFocusableWhenDisabled.mjs var React13 = __toESM(require_react(), 1); function useFocusableWhenDisabled(parameters) { const { @@ -1346,7 +1527,30 @@ function useFocusableWhenDisabled(parameters) { }; } -// node_modules/@base-ui/react/internals/use-button/useButton.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/owner.mjs +function ownerDocument(node) { + return node?.ownerDocument || document; +} + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/utils/dispatchClickWithModifiers.mjs +function dispatchClickWithModifiers(target, sourceEvent, { + detail = 0, + pointerType = "" +} = {}) { + target.dispatchEvent(new (getWindow(target)).PointerEvent("click", { + bubbles: true, + cancelable: true, + composed: true, + detail, + pointerType, + shiftKey: sourceEvent.shiftKey, + ctrlKey: sourceEvent.ctrlKey, + altKey: sourceEvent.altKey, + metaKey: sourceEvent.metaKey + })); +} + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/use-button/useButton.mjs function useButton(parameters = {}) { const { disabled: disabled2 = false, @@ -1441,22 +1645,25 @@ function useButton(parameters = {}) { return; } event.preventDefault(); - if (isLink || isNativeButton && isButton) { - currentTarget.click(); - event.preventBaseUIHandler(); - } else if (shouldClick) { - externalOnClick?.(event); + if (!isNativeButton || isButton) { event.preventBaseUIHandler(); + dispatchClickWithModifiers(currentTarget, event); } return; } - if (shouldClick) { - if (!isNativeButton && (isSpaceKey || isEnterKey)) { + if (!shouldClick || isNativeButton || !isSpaceKey && !isEnterKey) { + if (isCurrentTarget && isLink && isSpaceKey) { event.preventDefault(); } - if (!isNativeButton && isEnterKey) { - externalOnClick?.(event); - } + return; + } + if (event.defaultPrevented) { + return; + } + event.preventDefault(); + if (isEnterKey) { + event.preventBaseUIHandler(); + dispatchClickWithModifiers(currentTarget, event); } }, onKeyUp(event) { @@ -1472,8 +1679,9 @@ function useButton(parameters = {}) { if (event.baseUIHandlerPrevented) { return; } - if (event.target === event.currentTarget && !isNativeButton && !isCompositeItem && event.key === " ") { - externalOnClick?.(event); + if (event.target === event.currentTarget && !isNativeButton && !isCompositeItem && !event.defaultPrevented && event.key === " ") { + event.preventBaseUIHandler(); + dispatchClickWithModifiers(event.currentTarget, event); } }, onPointerDown(event) { @@ -1502,10 +1710,10 @@ function isButtonElement(elem) { return isHTMLElement(elem) && elem.tagName === "BUTTON"; } function isValidLinkElement(elem) { - return Boolean(elem?.tagName === "A" && elem?.href); + return isHTMLElement(elem) && elem.tagName === "A" && Boolean(elem.href); } -// node_modules/@base-ui/utils/addEventListener.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/addEventListener.mjs function addEventListener(target, type, listener, options) { target.addEventListener(type, listener, options); return () => { @@ -1513,7 +1721,7 @@ function addEventListener(target, type, listener, options) { }; } -// node_modules/@base-ui/utils/useValueAsRef.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/useValueAsRef.mjs function useValueAsRef(value) { const latest = useRefWithInit(createLatestRef, value).current; latest.next = value; @@ -1531,127 +1739,17 @@ function createLatestRef(value) { return latest; } -// node_modules/@base-ui/utils/owner.mjs -function ownerDocument(node) { - return node?.ownerDocument || document; -} - -// node_modules/@base-ui/react/internals/useOpenChangeComplete.mjs -var React15 = __toESM(require_react(), 1); - -// node_modules/@base-ui/react/internals/useAnimationsFinished.mjs -var ReactDOM = __toESM(require_react_dom(), 1); - -// node_modules/@base-ui/react/utils/resolveRef.mjs -function resolveRef(maybeRef) { - if (maybeRef == null) { - return maybeRef; - } - return "current" in maybeRef ? maybeRef.current : maybeRef; -} - -// node_modules/@base-ui/react/internals/useAnimationsFinished.mjs -function useAnimationsFinished(elementOrRef, waitForStartingStyleRemoved = false, treatAbortedAsFinished = true) { - const frame = useAnimationFrame(); - return useStableCallback((fnToExecute, signal = null) => { - frame.cancel(); - const element = resolveRef(elementOrRef); - if (element == null) { - return; - } - const resolvedElement = element; - const done = () => { - ReactDOM.flushSync(fnToExecute); - }; - if (typeof resolvedElement.getAnimations !== "function" || globalThis.BASE_UI_ANIMATIONS_DISABLED) { - fnToExecute(); - return; - } - function exec() { - Promise.all(resolvedElement.getAnimations().map((animation) => animation.finished)).then(() => { - if (!signal?.aborted) { - done(); - } - }).catch(() => { - if (treatAbortedAsFinished) { - if (!signal?.aborted) { - done(); - } - return; - } - const currentAnimations = resolvedElement.getAnimations(); - if (!signal?.aborted && currentAnimations.length > 0 && currentAnimations.some((animation) => animation.pending || animation.playState !== "finished")) { - exec(); - } - }); - } - if (waitForStartingStyleRemoved) { - const startingStyleAttribute = TransitionStatusDataAttributes.startingStyle; - if (!resolvedElement.hasAttribute(startingStyleAttribute)) { - frame.request(exec); - return; - } - const attributeObserver = new MutationObserver(() => { - if (!resolvedElement.hasAttribute(startingStyleAttribute)) { - attributeObserver.disconnect(); - exec(); - } - }); - attributeObserver.observe(resolvedElement, { - attributes: true, - attributeFilter: [startingStyleAttribute] - }); - signal?.addEventListener("abort", () => attributeObserver.disconnect(), { - once: true - }); - return; - } - frame.request(exec); - }); -} - -// node_modules/@base-ui/react/internals/useOpenChangeComplete.mjs -function useOpenChangeComplete(parameters) { - const { - enabled = true, - open, - ref, - onComplete: onCompleteParam - } = parameters; - const onComplete = useStableCallback(onCompleteParam); - const runOnceAnimationsFinish = useAnimationsFinished(ref, open, false); - React15.useEffect(() => { - if (!enabled) { - return void 0; - } - const abortController = new AbortController(); - runOnceAnimationsFinish(onComplete, abortController.signal); - return () => { - abortController.abort(); - }; - }, [enabled, open, onComplete, runOnceAnimationsFinish]); -} - -// node_modules/@base-ui/utils/useOnFirstRender.mjs -var React16 = __toESM(require_react(), 1); -function useOnFirstRender(fn) { - const ref = React16.useRef(true); - if (ref.current) { - ref.current = false; - fn(); - } -} - -// node_modules/@base-ui/utils/platform/parts.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/platform/parts.mjs var parts_exports = {}; __export(parts_exports, { engine: () => engine_exports, env: () => env_exports, + mediaQuery: () => media_query_exports, os: () => os_exports, screenReader: () => screen_reader_exports }); -// node_modules/@base-ui/utils/platform/os.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/platform/os.mjs var os_exports = {}; __export(os_exports, { android: () => android, @@ -1662,7 +1760,7 @@ __export(os_exports, { windows: () => windows }); -// node_modules/@base-ui/utils/platform/shared.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/platform/shared.mjs function readRawData() { if (typeof navigator === "undefined") { return { @@ -1698,7 +1796,7 @@ var { var lowerUserAgent = userAgent.toLowerCase(); var lowerPlatform = platform.toLowerCase(); -// node_modules/@base-ui/utils/platform/os.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/platform/os.mjs var ios = /^i(os$|p)/.test(lowerPlatform) || lowerPlatform === "macintel" && maxTouchPoints > 1; var ANDROID_STRING = "android"; var android = lowerPlatform === ANDROID_STRING || lowerUserAgent.includes(ANDROID_STRING); @@ -1707,7 +1805,7 @@ var windows = lowerPlatform.startsWith("win"); var linux = !android && /^(linux|chrome os)/.test(lowerPlatform); var apple = mac || ios; -// node_modules/@base-ui/utils/platform/engine.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/platform/engine.mjs var engine_exports = {}; __export(engine_exports, { blink: () => blink, @@ -1718,63 +1816,43 @@ var webkit = typeof CSS !== "undefined" && !!CSS.supports?.("-webkit-backdrop-fi var gecko = !webkit && lowerUserAgent.includes("firefox"); var blink = !webkit && lowerUserAgent.includes("chrom"); -// node_modules/@base-ui/utils/platform/screen-reader.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/platform/screen-reader.mjs var screen_reader_exports = {}; __export(screen_reader_exports, { voiceOver: () => voiceOver }); var voiceOver = apple; -// node_modules/@base-ui/utils/platform/env.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/platform/env.mjs var env_exports = {}; __export(env_exports, { jsdom: () => jsdom }); var jsdom = /jsdom|happydom/.test(lowerUserAgent); -// node_modules/@base-ui/utils/useTimeout.mjs -var EMPTY3 = 0; -var Timeout = class _Timeout { - static create() { - return new _Timeout(); - } - currentId = EMPTY3; - /** - * Executes `fn` after `delay`, clearing any previously scheduled call. - */ - start(delay, fn) { - this.clear(); - this.currentId = setTimeout(() => { - this.currentId = EMPTY3; - fn(); - }, delay); - } - isStarted() { - return this.currentId !== EMPTY3; - } - clear = () => { - if (this.currentId !== EMPTY3) { - clearTimeout(this.currentId); - this.currentId = EMPTY3; - } - }; - disposeEffect = () => { - return this.clear; - }; -}; -function useTimeout() { - const timeout = useRefWithInit(Timeout.create).current; - useOnMount(timeout.disposeEffect); - return timeout; -} +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/platform/media-query.mjs +var media_query_exports = {}; +__export(media_query_exports, { + iOS: () => iOS +}); +var iOS = "@supports (-webkit-touch-callout: none)"; -// node_modules/@base-ui/react/floating-ui-react/components/FloatingDelayGroup.mjs -var React17 = __toESM(require_react(), 1); +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/components/FloatingDelayGroup.mjs +var React15 = __toESM(require_react(), 1); -// node_modules/@base-ui/react/floating-ui-react/utils/event.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/utils/event.mjs function isReactEvent(event) { return "nativeEvent" in event; } +function isVirtualClick(event) { + if (event.pointerType === "" && event.isTrusted) { + return true; + } + if (parts_exports.os.android && event.pointerType) { + return event.type === "click" && event.buttons === 1; + } + return event.detail === 0 && !event.pointerType; +} function isMouseLikePointerType(pointerType, strict) { const values = ["mouse", "pen"]; if (!strict) { @@ -1787,11 +1865,7 @@ function isClickLikeEvent(event) { return type === "click" || type === "mousedown" || type === "keydown" || type === "keyup"; } -// node_modules/@base-ui/react/floating-ui-react/utils/constants.mjs -var FOCUSABLE_ATTRIBUTE = "data-base-ui-focusable"; -var TYPEABLE_SELECTOR = "input:not([type='hidden']):not([disabled]),[contenteditable]:not([contenteditable='false']),textarea:not([disabled])"; - -// node_modules/@base-ui/react/internals/shadowDom.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/shadowDom.mjs function activeElement(doc) { let element = doc.activeElement; while (element?.shadowRoot?.activeElement != null) { @@ -1818,25 +1892,101 @@ function contains(parent, child) { } return false; } +function closest(node, selector) { + let current = node; + while (current) { + if (isElement(current) && current.matches(selector)) { + return current; + } + current = current.assignedSlot ?? current.parentNode ?? (isShadowRoot(current) ? current.host : null); + } + return null; +} function getTarget(event) { if ("composedPath" in event) { - return event.composedPath()[0]; + return event.composedPath()[0] ?? event.target; } return event.target; } -// node_modules/@base-ui/react/floating-ui-react/utils/element.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/utils/constants.mjs +var FOCUSABLE_ATTRIBUTE = "data-base-ui-focusable"; +var TYPEABLE_SELECTOR = "input:not([type='hidden']):not([disabled]),[contenteditable]:not([contenteditable='false']),textarea:not([disabled])"; + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/utils/CommonPopupDataAttributes.mjs +var open = "data-open"; +var closed = "data-closed"; +var anchorHidden = "data-anchor-hidden"; + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/utils/CommonTriggerDataAttributes.mjs +var CommonTriggerDataAttributes_exports = {}; +__export(CommonTriggerDataAttributes_exports, { + popupOpen: () => popupOpen, + pressed: () => pressed +}); +var popupOpen = "data-popup-open"; +var pressed = "data-pressed"; + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/utils/popupStateMapping.mjs +var TRIGGER_HOOK = { + [popupOpen]: "" +}; +var PRESSABLE_TRIGGER_HOOK = { + [popupOpen]: "", + [pressed]: "" +}; +var POPUP_OPEN_HOOK = { + [open]: "" +}; +var POPUP_CLOSED_HOOK = { + [closed]: "" +}; +var ANCHOR_HIDDEN_HOOK = { + [anchorHidden]: "" +}; +var triggerOpenStateMapping = { + open(value) { + if (value) { + return TRIGGER_HOOK; + } + return null; + } +}; +var popupStateMapping = { + open(value) { + if (value) { + return POPUP_OPEN_HOOK; + } + return POPUP_CLOSED_HOOK; + }, + anchorHidden(value) { + if (value) { + return ANCHOR_HIDDEN_HOOK; + } + return null; + } +}; +var popupTransitionStateMapping = { + ...popupStateMapping, + ...transitionStatusMapping +}; + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/tooltip/trigger/TooltipTriggerDataAttributes.mjs +var popupOpen2 = CommonTriggerDataAttributes_exports.popupOpen; +var triggerDisabled = "data-trigger-disabled"; + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/utils/element.mjs function isTargetInsideEnabledTrigger(target, triggerElements) { if (!isElement(target)) { return false; } const targetElement = target; if (triggerElements.hasElement(targetElement)) { - return !targetElement.hasAttribute("data-trigger-disabled"); + return !targetElement.hasAttribute(triggerDisabled); } for (const [, trigger] of triggerElements.entries()) { if (contains(trigger, targetElement)) { - return !trigger.hasAttribute("data-trigger-disabled"); + return !trigger.hasAttribute(triggerDisabled); } } return false; @@ -1858,7 +2008,7 @@ function isTypeableElement(element) { return isHTMLElement(element) && element.matches(TYPEABLE_SELECTOR); } function isInteractiveElement(element) { - return element?.closest(`button,a[href],[role="button"],select,[tabindex]:not([tabindex="-1"]),${TYPEABLE_SELECTOR}`) != null; + return closest(element, `button,a[href],[role="button"],select,[tabindex]:not([tabindex="-1"]),${TYPEABLE_SELECTOR}`) != null; } function matchesFocusVisible(element) { if (!element || parts_exports.env.jsdom) { @@ -1871,7 +2021,7 @@ function matchesFocusVisible(element) { } } -// node_modules/@base-ui/react/floating-ui-react/hooks/useHoverShared.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/hooks/useHoverShared.mjs function resolveValue(value, pointerType) { if (pointerType != null && !isMouseLikePointerType(pointerType)) { return 0; @@ -1901,9 +2051,9 @@ function isHoverOpenEvent(openEventType) { return openEventType?.includes("mouse") && openEventType !== "mousedown"; } -// node_modules/@base-ui/react/floating-ui-react/components/FloatingDelayGroup.mjs -var import_jsx_runtime = __toESM(require_jsx_runtime(), 1); -var FloatingDelayGroupContext = /* @__PURE__ */ React17.createContext({ +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/components/FloatingDelayGroup.mjs +var import_jsx_runtime2 = __toESM(require_jsx_runtime(), 1); +var FloatingDelayGroupContext = /* @__PURE__ */ React15.createContext({ hasProvider: false, timeoutMs: 0, delayRef: { @@ -1930,10 +2080,10 @@ function FloatingDelayGroup(props) { delay, timeoutMs = 0 } = props; - const delayRef = React17.useRef(delay); - const initialDelayRef = React17.useRef(delay); - const currentIdRef = React17.useRef(null); - const currentContextRef = React17.useRef(null); + const delayRef = React15.useRef(delay); + const initialDelayRef = React15.useRef(delay); + const currentIdRef = React15.useRef(null); + const currentContextRef = React15.useRef(null); const timeout = useTimeout(); useIsoLayoutEffect(() => { initialDelayRef.current = delay; @@ -1946,8 +2096,8 @@ function FloatingDelayGroup(props) { close: getDelay(delay, "close") }; }, [delay, currentIdRef, delayRef, initialDelayRef]); - return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FloatingDelayGroupContext.Provider, { - value: React17.useMemo(() => ({ + return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FloatingDelayGroupContext.Provider, { + value: React15.useMemo(() => ({ hasProvider: true, delayRef, initialDelayRef, @@ -1959,15 +2109,14 @@ function FloatingDelayGroup(props) { children }); } -function useDelayGroup(context, options = { +function useDelayGroup(store2, options = { open: false }) { const { - open + open: open2 } = options; - const store2 = "rootStore" in context ? context.rootStore : context; const floatingId = store2.useState("floatingId"); - const groupContext = React17.useContext(FloatingDelayGroupContext); + const groupContext = React15.useContext(FloatingDelayGroupContext); const { currentIdRef, delayRef, @@ -1977,22 +2126,13 @@ function useDelayGroup(context, options = { hasProvider, timeout } = groupContext; - const [isInstantPhase, setIsInstantPhase] = React17.useState(false); - const openRef = React17.useRef(open); - const isUnmountedRef = React17.useRef(false); - useIsoLayoutEffect(() => { - openRef.current = open; - }, [open]); + const [isInstantPhase, setIsInstantPhase] = React15.useState(false); + const openRef = React15.useRef(open2); useIsoLayoutEffect(() => { - return () => { - isUnmountedRef.current = true; - }; - }, []); + openRef.current = open2; + }, [open2]); useIsoLayoutEffect(() => { function unset() { - if (!isUnmountedRef.current) { - setIsInstantPhase(false); - } currentContextRef.current?.setIsInstantPhase(false); currentIdRef.current = null; currentContextRef.current = null; @@ -2002,7 +2142,7 @@ function useDelayGroup(context, options = { if (!currentIdRef.current) { return void 0; } - if (!open && currentIdRef.current === floatingId) { + if (!open2 && currentIdRef.current === floatingId) { setIsInstantPhase(false); if (timeoutMs) { const closingId = floatingId; @@ -2021,9 +2161,9 @@ function useDelayGroup(context, options = { unset(); } return void 0; - }, [open, floatingId, currentIdRef, delayRef, timeoutMs, initialDelayRef, currentContextRef, timeout, store2]); + }, [open2, floatingId, currentIdRef, delayRef, timeoutMs, initialDelayRef, currentContextRef, timeout, store2]); useIsoLayoutEffect(() => { - if (!open) { + if (!open2) { return; } const prevContext = currentContextRef.current; @@ -2046,7 +2186,7 @@ function useDelayGroup(context, options = { setIsInstantPhase(false); prevContext?.setIsInstantPhase(false); } - }, [open, floatingId, store2, currentIdRef, delayRef, initialDelayRef, currentContextRef, timeout]); + }, [open2, floatingId, store2, currentIdRef, delayRef, initialDelayRef, currentContextRef, timeout]); useIsoLayoutEffect(() => { return () => { if (currentIdRef.current === floatingId) { @@ -2060,14 +2200,15 @@ function useDelayGroup(context, options = { } }; }, [currentContextRef, currentIdRef, delayRef, floatingId, initialDelayRef, timeout]); - return React17.useMemo(() => ({ + return React15.useMemo(() => ({ + activeIdRef: currentIdRef, hasProvider, delayRef, isInstantPhase - }), [hasProvider, delayRef, isInstantPhase]); + }), [currentIdRef, hasProvider, delayRef, isInstantPhase]); } -// node_modules/@base-ui/utils/mergeCleanups.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/mergeCleanups.mjs function mergeCleanups(...cleanups) { return () => { for (let i = 0; i < cleanups.length; i += 1) { @@ -2079,10 +2220,10 @@ function mergeCleanups(...cleanups) { }; } -// node_modules/@base-ui/react/utils/FocusGuard.mjs -var React18 = __toESM(require_react(), 1); +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/utils/FocusGuard.mjs +var React16 = __toESM(require_react(), 1); -// node_modules/@base-ui/utils/visuallyHidden.mjs +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/visuallyHidden.mjs var visuallyHiddenBase = { clipPath: "inset(50%)", overflow: "hidden", @@ -2096,6 +2237,7 @@ var visuallyHiddenBase = { var visuallyHidden = { ...visuallyHiddenBase, position: "fixed", + margin: 0, top: 0, left: 0 }; @@ -2104,10 +2246,10 @@ var visuallyHiddenInput = { position: "absolute" }; -// node_modules/@base-ui/react/utils/FocusGuard.mjs -var import_jsx_runtime2 = __toESM(require_jsx_runtime(), 1); -var FocusGuard = /* @__PURE__ */ React18.forwardRef(function FocusGuard2(props, ref) { - const [role, setRole] = React18.useState(); +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/utils/FocusGuard.mjs +var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1); +var FocusGuard = /* @__PURE__ */ React16.forwardRef(function FocusGuard2(props, ref) { + const [role, setRole] = React16.useState(); useIsoLayoutEffect(() => { if (parts_exports.screenReader.voiceOver && parts_exports.engine.webkit) { setRole("button"); @@ -2118,7 +2260,7 @@ var FocusGuard = /* @__PURE__ */ React18.forwardRef(function FocusGuard2(props, // Role is only for VoiceOver role }; - return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { + return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { ...props, ref, style: visuallyHidden, @@ -2129,8 +2271,7 @@ var FocusGuard = /* @__PURE__ */ React18.forwardRef(function FocusGuard2(props, }); if (true) FocusGuard.displayName = "FocusGuard"; -// node_modules/@floating-ui/utils/dist/floating-ui.utils.mjs -var sides = ["top", "right", "bottom", "left"]; +// node_modules/.store/@floating-ui/utils@0.2.12-gOxTzMf36nCrbeHpEew-rw/node_modules/@floating-ui/utils/dist/floating-ui.utils.mjs var min = Math.min; var max = Math.max; var round = Math.round; @@ -2223,12 +2364,12 @@ function getOppositePlacement(placement) { return oppositeSideMap[side] + placement.slice(side.length); } function expandPaddingObject(padding) { + var _padding$top, _padding$right, _padding$bottom, _padding$left; return { - top: 0, - right: 0, - bottom: 0, - left: 0, - ...padding + top: (_padding$top = padding.top) != null ? _padding$top : 0, + right: (_padding$right = padding.right) != null ? _padding$right : 0, + bottom: (_padding$bottom = padding.bottom) != null ? _padding$bottom : 0, + left: (_padding$left = padding.left) != null ? _padding$left : 0 }; } function getPaddingObject(padding) { @@ -2258,7 +2399,7 @@ function rectToClientRect(rect) { }; } -// node_modules/@base-ui/react/floating-ui-react/utils/composite.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/utils/composite.mjs function isHiddenByStyles(styles) { return styles.visibility === "hidden" || styles.visibility === "collapse"; } @@ -2272,7 +2413,7 @@ function isElementVisible(element, styles = element ? getComputedStyle2(element) return styles.display !== "none" && styles.display !== "contents"; } -// node_modules/@base-ui/react/floating-ui-react/utils/tabbable.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/utils/tabbable.mjs var CANDIDATE_SELECTOR = 'a[href],button,input,select,textarea,summary,details,iframe,object,embed,[tabindex],[contenteditable]:not([contenteditable="false"]),audio[controls],video[controls]'; function getParentElement(element) { const assignedSlot = element.assignedSlot; @@ -2440,22 +2581,22 @@ function enableFocusInside(container) { }); } -// node_modules/@base-ui/react/floating-ui-react/utils/nodes.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/utils/nodes.mjs function getNodeChildren(nodes, id, onlyOpenChildren = true) { const directChildren = nodes.filter((node) => node.parentId === id); return directChildren.flatMap((child) => [...!onlyOpenChildren || child.context?.open ? [child] : [], ...getNodeChildren(nodes, child.id, onlyOpenChildren)]); } -// node_modules/@base-ui/react/floating-ui-react/utils/createAttribute.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/utils/createAttribute.mjs function createAttribute(name) { return `data-base-ui-${name}`; } -// node_modules/@base-ui/react/floating-ui-react/components/FloatingPortal.mjs -var React19 = __toESM(require_react(), 1); +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/components/FloatingPortal.mjs +var React17 = __toESM(require_react(), 1); var ReactDOM2 = __toESM(require_react_dom(), 1); -// node_modules/@base-ui/react/internals/constants.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/constants.mjs var DISABLED_TRANSITIONS_STYLE = { style: { transition: "none" @@ -2475,11 +2616,11 @@ var ownerVisuallyHidden = { left: 0 }; -// node_modules/@base-ui/react/floating-ui-react/components/FloatingPortal.mjs -var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1); -var PortalContext = /* @__PURE__ */ React19.createContext(null); +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/components/FloatingPortal.mjs +var import_jsx_runtime4 = __toESM(require_jsx_runtime(), 1); +var PortalContext = /* @__PURE__ */ React17.createContext(null); if (true) PortalContext.displayName = "PortalContext"; -var usePortalContext = () => React19.useContext(PortalContext); +var usePortalContext = () => React17.useContext(PortalContext); var attr = createAttribute("portal"); function useFloatingPortalNode(props = {}) { const { @@ -2491,14 +2632,14 @@ function useFloatingPortalNode(props = {}) { const uniqueId = useId(); const portalContext = usePortalContext(); const parentPortalNode = portalContext?.portalNode; - const [containerElement, setContainerElement] = React19.useState(null); - const [portalNode, setPortalNode] = React19.useState(null); + const [containerElement, setContainerElement] = React17.useState(null); + const [portalNode, setPortalNode] = React17.useState(null); const setPortalNodeRef = useStableCallback((node) => { if (node !== null) { setPortalNode(node); } }); - const containerRef = React19.useRef(null); + const containerRef = React17.useRef(null); useIsoLayoutEffect(() => { if (containerProp === null) { if (containerRef.current) { @@ -2508,9 +2649,6 @@ function useFloatingPortalNode(props = {}) { } return; } - if (uniqueId == null) { - return; - } const resolvedContainer = (containerProp && (isNode(containerProp) ? containerProp : containerProp.current)) ?? parentPortalNode ?? document.body; if (resolvedContainer == null) { if (containerRef.current) { @@ -2525,7 +2663,7 @@ function useFloatingPortalNode(props = {}) { setPortalNode(null); setContainerElement(resolvedContainer); } - }, [containerProp, parentPortalNode, uniqueId]); + }, [containerProp, parentPortalNode]); const portalElement = useRenderElement("div", componentProps, { ref: [ref, setPortalNodeRef], props: [{ @@ -2535,39 +2673,43 @@ function useFloatingPortalNode(props = {}) { }); const portalSubtree = containerElement && portalElement ? /* @__PURE__ */ ReactDOM2.createPortal(portalElement, containerElement) : null; return { - portalNode, - portalSubtree + node: portalNode, + // `id` and `render` props can override or remove the generated ID. Use the exact + // rendered value so `aria-owns` never points at an ID absent from the DOM. + nodeId: /* @__PURE__ */ React17.isValidElement(portalElement) ? portalElement.props.id : void 0, + subtree: portalSubtree }; } -var FloatingPortal = /* @__PURE__ */ React19.forwardRef(function FloatingPortal2(componentProps, forwardedRef) { +var FloatingPortal = /* @__PURE__ */ React17.forwardRef(function FloatingPortal2(componentProps, forwardedRef) { const { render, className, style, children, container, - renderGuards, + portalOwnerRole, ...elementProps } = componentProps; const { - portalNode, - portalSubtree + node: portalNode, + nodeId: portalNodeId, + subtree: portalSubtree } = useFloatingPortalNode({ container, ref: forwardedRef, componentProps, elementProps }); - const beforeOutsideRef = React19.useRef(null); - const afterOutsideRef = React19.useRef(null); - const beforeInsideRef = React19.useRef(null); - const afterInsideRef = React19.useRef(null); - const [focusManagerState, setFocusManagerState] = React19.useState(null); - const focusInsideDisabledRef = React19.useRef(false); + const beforeOutsideRef = React17.useRef(null); + const afterOutsideRef = React17.useRef(null); + const beforeInsideRef = React17.useRef(null); + const afterInsideRef = React17.useRef(null); + const [focusManagerState, setFocusManagerState] = React17.useState(null); + const focusInsideDisabledRef = React17.useRef(false); const modal = focusManagerState?.modal; - const open = focusManagerState?.open; - const shouldRenderGuards = typeof renderGuards === "boolean" ? renderGuards : !!focusManagerState && !focusManagerState.modal && focusManagerState.open && !!portalNode; - React19.useEffect(() => { + const open2 = focusManagerState?.open; + const shouldRenderGuards = !!focusManagerState && !focusManagerState.modal && focusManagerState.open && !!portalNode; + React17.useEffect(() => { if (!portalNode || modal) { return void 0; } @@ -2587,13 +2729,13 @@ var FloatingPortal = /* @__PURE__ */ React19.forwardRef(function FloatingPortal2 return mergeCleanups(addEventListener(portalNode, "focusin", onFocus, true), addEventListener(portalNode, "focusout", onFocus, true)); }, [portalNode, modal]); useIsoLayoutEffect(() => { - if (!portalNode || open !== true || !focusInsideDisabledRef.current) { + if (!portalNode || open2 !== true || !focusInsideDisabledRef.current) { return; } enableFocusInside(portalNode); focusInsideDisabledRef.current = false; - }, [open, portalNode]); - const portalContextValue = React19.useMemo(() => ({ + }, [open2, portalNode]); + const portalContextValue = React17.useMemo(() => ({ beforeOutsideRef, afterOutsideRef, beforeInsideRef, @@ -2601,10 +2743,10 @@ var FloatingPortal = /* @__PURE__ */ React19.forwardRef(function FloatingPortal2 portalNode, setFocusManagerState }), [portalNode]); - return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(React19.Fragment, { - children: [portalSubtree, /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PortalContext.Provider, { + return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(React17.Fragment, { + children: [portalSubtree, /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(PortalContext.Provider, { value: portalContextValue, - children: [shouldRenderGuards && portalNode && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FocusGuard, { + children: [shouldRenderGuards && portalNode && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(FocusGuard, { "data-type": "outside", ref: beforeOutsideRef, onFocus: (event) => { @@ -2616,10 +2758,11 @@ var FloatingPortal = /* @__PURE__ */ React19.forwardRef(function FloatingPortal2 prevTabbable?.focus(); } } - }), shouldRenderGuards && portalNode && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { - "aria-owns": portalNode.id, + }), shouldRenderGuards && portalNode && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { + role: portalOwnerRole, + "aria-owns": portalNodeId, style: ownerVisuallyHidden - }), portalNode && /* @__PURE__ */ ReactDOM2.createPortal(children, portalNode), shouldRenderGuards && portalNode && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FocusGuard, { + }), portalNode && /* @__PURE__ */ ReactDOM2.createPortal(children, portalNode), shouldRenderGuards && portalNode && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(FocusGuard, { "data-type": "outside", ref: afterOutsideRef, onFocus: (event) => { @@ -2640,10 +2783,10 @@ var FloatingPortal = /* @__PURE__ */ React19.forwardRef(function FloatingPortal2 }); if (true) FloatingPortal.displayName = "FloatingPortal"; -// node_modules/@base-ui/react/floating-ui-react/components/FloatingTree.mjs -var React20 = __toESM(require_react(), 1); +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/components/FloatingTree.mjs +var React18 = __toESM(require_react(), 1); -// node_modules/@base-ui/react/floating-ui-react/utils/createEventEmitter.mjs +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/utils/createEventEmitter.mjs function createEventEmitter() { const map = /* @__PURE__ */ new Map(); return { @@ -2662,20 +2805,20 @@ function createEventEmitter() { }; } -// node_modules/@base-ui/react/floating-ui-react/components/FloatingTree.mjs -var import_jsx_runtime4 = __toESM(require_jsx_runtime(), 1); -var FloatingNodeContext = /* @__PURE__ */ React20.createContext(null); +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/components/FloatingTree.mjs +var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1); +var FloatingNodeContext = /* @__PURE__ */ React18.createContext(null); if (true) FloatingNodeContext.displayName = "FloatingNodeContext"; -var FloatingTreeContext = /* @__PURE__ */ React20.createContext(null); +var FloatingTreeContext = /* @__PURE__ */ React18.createContext(null); if (true) FloatingTreeContext.displayName = "FloatingTreeContext"; -var useFloatingParentNodeId = () => React20.useContext(FloatingNodeContext)?.id || null; +var useFloatingParentNodeId = () => React18.useContext(FloatingNodeContext)?.id || null; var useFloatingTree = (externalTree) => { - const contextTree = React20.useContext(FloatingTreeContext); + const contextTree = React18.useContext(FloatingTreeContext); return externalTree ?? contextTree; }; -// node_modules/@base-ui/react/floating-ui-react/hooks/useClientPoint.mjs -var React21 = __toESM(require_react(), 1); +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/hooks/useClientPoint.mjs +var React19 = __toESM(require_react(), 1); function createVirtualElement(domElement, data) { let offsetX = null; let offsetY = null; @@ -2692,8 +2835,8 @@ function createVirtualElement(domElement, data) { const isXAxis = data.axis === "x" || data.axis === "both"; const isYAxis = data.axis === "y" || data.axis === "both"; const canTrackCursorOnAutoUpdate = ["mouseenter", "mousemove"].includes(data.dataRef.current.openEvent?.type || "") && data.pointerType !== "touch"; - let width = domRect.width; - let height = domRect.height; + const width = data.axis === "y" ? domRect.width : 0; + const height = data.axis === "x" ? domRect.height : 0; let x = domRect.x; let y = domRect.y; if (offsetX == null && data.x && isXAxis) { @@ -2704,16 +2847,9 @@ function createVirtualElement(domElement, data) { } x -= offsetX || 0; y -= offsetY || 0; - width = 0; - height = 0; if (!isAutoUpdateEvent || canTrackCursorOnAutoUpdate) { - width = data.axis === "y" ? domRect.width : 0; - height = data.axis === "x" ? domRect.height : 0; x = isXAxis && data.x != null ? data.x : x; y = isYAxis && data.y != null ? data.y : y; - } else if (isAutoUpdateEvent && !canTrackCursorOnAutoUpdate) { - height = data.axis === "x" ? domRect.height : height; - width = data.axis === "y" ? domRect.width : width; } isAutoUpdateEvent = true; return { @@ -2732,20 +2868,19 @@ function createVirtualElement(domElement, data) { function isMouseBasedEvent(event) { return event != null && event.clientX != null; } -function useClientPoint(context, props = {}) { +function useClientPoint(store2, props = {}) { const { enabled = true, axis = "both" } = props; - const store2 = "rootStore" in context ? context.rootStore : context; - const open = store2.useState("open"); + const open2 = store2.useState("open"); const floating = store2.useState("floatingElement"); const domReference = store2.useState("domReferenceElement"); const dataRef = store2.context.dataRef; - const initialRef = React21.useRef(false); - const cleanupListenerRef = React21.useRef(null); - const [pointerType, setPointerType] = React21.useState(); - const [reactive, setReactive] = React21.useState([]); + const initialRef = React19.useRef(false); + const cleanupListenerRef = React19.useRef(null); + const [pointerType, setPointerType] = React19.useState(); + const [reactive, setReactive] = React19.useState([]); const resetReference = useStableCallback((reference2) => { store2.set("positionReference", reference2); }); @@ -2765,15 +2900,15 @@ function useClientPoint(context, props = {}) { })); }); const handleReferenceEnterOrMove = useStableCallback((event) => { - if (!open) { + if (!open2) { setReference(event.clientX, event.clientY, event.currentTarget); } else if (!cleanupListenerRef.current) { setReference(event.clientX, event.clientY, event.currentTarget); setReactive([]); } }); - const openCheck = isMouseLikePointerType(pointerType) ? floating : open; - React21.useEffect(() => { + const openCheck = isMouseLikePointerType(pointerType) ? floating : open2; + React19.useEffect(() => { if (!enabled) { resetReference(domReference); return void 0; @@ -2801,20 +2936,20 @@ function useClientPoint(context, props = {}) { } return cleanupListener; }, [openCheck, enabled, floating, dataRef, domReference, store2, setReference, resetReference, reactive]); - React21.useEffect(() => () => { + React19.useEffect(() => () => { store2.set("positionReference", null); }, [store2]); - React21.useEffect(() => { + React19.useEffect(() => { if (enabled && !floating) { initialRef.current = false; } }, [enabled, floating]); - React21.useEffect(() => { - if (!enabled && open) { + React19.useEffect(() => { + if (!enabled && open2) { initialRef.current = true; } - }, [enabled, open]); - const reference = React21.useMemo(() => { + }, [enabled, open2]); + const reference = React19.useMemo(() => { function setPointerTypeRef(event) { setPointerType(event.pointerType); } @@ -2825,14 +2960,14 @@ function useClientPoint(context, props = {}) { onMouseEnter: handleReferenceEnterOrMove }; }, [handleReferenceEnterOrMove]); - return React21.useMemo(() => enabled ? { + return React19.useMemo(() => enabled ? { reference, trigger: reference } : {}, [enabled, reference]); } -// node_modules/@base-ui/react/floating-ui-react/hooks/useDismiss.mjs -var React22 = __toESM(require_react(), 1); +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/hooks/useDismiss.mjs +var React20 = __toESM(require_react(), 1); function alwaysFalse() { return false; } @@ -2842,7 +2977,7 @@ function normalizeProp(normalizable) { outsidePress: typeof normalizable === "boolean" ? normalizable : normalizable?.outsidePress ?? true }; } -function useDismiss(context, props = {}) { +function useDismiss(store2, props = {}) { const { enabled = true, escapeKey: escapeKey2 = true, @@ -2852,11 +2987,11 @@ function useDismiss(context, props = {}) { bubbles, externalTree } = props; - const store2 = "rootStore" in context ? context.rootStore : context; - const open = store2.useState("open"); + const open2 = store2.useState("open"); const floatingElement = store2.useState("floatingElement"); const { - dataRef + dataRef, + events } = store2.context; const tree = useFloatingTree(externalTree); const outsidePressFn = useStableCallback(typeof outsidePressProp === "function" ? outsidePressProp : () => false); @@ -2867,14 +3002,16 @@ function useDismiss(context, props = {}) { escapeKey: escapeKeyBubbles, outsidePress: outsidePressBubbles } = normalizeProp(bubbles); - const pressStartedInsideRef = React22.useRef(false); - const pressStartPreventedRef = React22.useRef(false); - const suppressNextOutsideClickRef = React22.useRef(false); - const isComposingRef = React22.useRef(false); - const currentPointerTypeRef = React22.useRef(""); - const touchStateRef = React22.useRef(null); + const pressStartedInsideRef = React20.useRef(false); + const pressStartPreventedRef = React20.useRef(false); + const suppressNextOutsideClickRef = React20.useRef(false); + const sawPressWhileOpenRef = React20.useRef(false); + const isComposingRef = React20.useRef(false); + const currentPointerTypeRef = React20.useRef(""); + const touchStateRef = React20.useRef(null); const cancelDismissOnEndTimeout = useTimeout(); const clearInsideReactTreeTimeout = useTimeout(); + const compositionTimeout = useTimeout(); const clearInsideReactTree = useStableCallback(() => { clearInsideReactTreeTimeout.clear(); dataRef.current.insideReactTree = false; @@ -2894,16 +3031,16 @@ function useDismiss(context, props = {}) { store2.setOpen(false, createChangeEventDetails(reason_parts_exports.triggerPress, event.nativeEvent)); }); const closeOnEscapeKeyDown = useStableCallback((event) => { - if (!open || !enabled || !escapeKey2 || event.key !== "Escape") { + if (!open2 || !enabled || !escapeKey2 || event.key !== "Escape") { return; } - if (isComposingRef.current) { + const native = isReactEvent(event) ? event.nativeEvent : event; + if (isComposingRef.current || native.isComposing) { return; } if (!escapeKeyBubbles && hasBlockingChild("__escapeKeyBubbles")) { return; } - const native = isReactEvent(event) ? event.nativeEvent : event; const eventDetails = createChangeEventDetails(reason_parts_exports.escapeKey, native); store2.setOpen(false, eventDetails); if (!eventDetails.isCanceled) { @@ -2918,7 +3055,7 @@ function useDismiss(context, props = {}) { clearInsideReactTreeTimeout.start(0, clearInsideReactTree); }); const markPressStartedInsideReactTree = useStableCallback((event) => { - if (!open || !enabled || event.button !== 0) { + if (!open2 || !enabled || event.button !== 0) { return; } const target = getTarget(event.nativeEvent); @@ -2931,7 +3068,7 @@ function useDismiss(context, props = {}) { } }); const markInsidePressStartPrevented = useStableCallback((event) => { - if (!open || !enabled) { + if (!open2 || !enabled) { return; } if (!(event.defaultPrevented || event.nativeEvent.defaultPrevented)) { @@ -2941,19 +3078,38 @@ function useDismiss(context, props = {}) { pressStartPreventedRef.current = true; } }); - React22.useEffect(() => { - if (!open || !enabled) { - return void 0; + React20.useEffect(() => { + function handleOpenChange(details) { + if (!details.open) { + sawPressWhileOpenRef.current = false; + } + } + events.on("openchange", handleOpenChange); + return () => { + events.off("openchange", handleOpenChange); + }; + }, [events]); + React20.useEffect(() => clearInsideReactTree, [clearInsideReactTree]); + React20.useEffect(() => { + if (!open2 || !enabled) { + if (!open2) { + sawPressWhileOpenRef.current = false; + isComposingRef.current = false; + currentPointerTypeRef.current = ""; + touchStateRef.current = null; + } + return clearInsideReactTree; } dataRef.current.__escapeKeyBubbles = escapeKeyBubbles; dataRef.current.__outsidePressBubbles = outsidePressBubbles; - const compositionTimeout = new Timeout(); const preventedPressSuppressionTimeout = new Timeout(); + const doc = ownerDocument(floatingElement); function handleCompositionStart() { compositionTimeout.clear(); isComposingRef.current = true; } function handleCompositionEnd() { + isComposingRef.current = true; compositionTimeout.start( // 0ms or 1ms don't work in Safari. 5ms appears to consistently work. // Only apply to WebKit for the test to remain 0ms. @@ -3035,8 +3191,8 @@ function useDismiss(context, props = {}) { const isScrollableY = lastTraversableNode || scrollRe.test(style.overflowY); const canScrollX = isScrollableX && target.clientWidth > 0 && target.scrollWidth > target.clientWidth; const canScrollY = isScrollableY && target.clientHeight > 0 && target.scrollHeight > target.clientHeight; - const isRTL2 = style.direction === "rtl"; - const pressedVerticalScrollbar = canScrollY && (isRTL2 ? event.offsetX <= target.offsetWidth - target.clientWidth : event.offsetX > target.clientWidth); + const isRTL3 = style.direction === "rtl"; + const pressedVerticalScrollbar = canScrollY && (isRTL3 ? event.offsetX <= target.offsetWidth - target.clientWidth : event.offsetX > target.clientWidth); const pressedHorizontalScrollbar = canScrollX && event.offsetY > target.clientHeight; if (pressedVerticalScrollbar || pressedHorizontalScrollbar) { return; @@ -3045,10 +3201,15 @@ function useDismiss(context, props = {}) { if (isEventWithinFloatingTree(event)) { return; } - if (getOutsidePressEvent() === "intentional" && suppressNextOutsideClickRef.current) { - preventedPressSuppressionTimeout.clear(); - suppressNextOutsideClickRef.current = false; - return; + if (getOutsidePressEvent() === "intentional") { + if (event.detail !== 0 && !isVirtualClick(event) && !sawPressWhileOpenRef.current) { + return; + } + if (suppressNextOutsideClickRef.current) { + preventedPressSuppressionTimeout.clear(); + suppressNextOutsideClickRef.current = false; + return; + } } if (typeof outsidePress2 === "function" && !outsidePress2(event)) { return; @@ -3103,6 +3264,9 @@ function useDismiss(context, props = {}) { function closeOnPressOutsideCapture(event) { cancelDismissOnEndTimeout.clear(); if (event.type === "pointerdown") { + if (event.button === 0) { + sawPressWhileOpenRef.current = true; + } currentPointerTypeRef.current = event.pointerType; } if (event.type === "mousedown" && touchStateRef.current && !touchStateRef.current.dismissOnMouseDown) { @@ -3117,6 +3281,9 @@ function useDismiss(context, props = {}) { }); } function handlePressEndCapture(event) { + if (event.type === "pointercancel") { + sawPressWhileOpenRef.current = false; + } if (!pressStartedInsideRef.current) { return; } @@ -3181,23 +3348,29 @@ function useDismiss(context, props = {}) { function handleTouchEndCapture(event) { addTargetEventListenerOnce(event, handleTouchEnd); } - const doc = ownerDocument(floatingElement); - const unsubscribe = mergeCleanups(escapeKey2 && mergeCleanups(addEventListener(doc, "keydown", closeOnEscapeKeyDown), addEventListener(doc, "compositionstart", handleCompositionStart), addEventListener(doc, "compositionend", handleCompositionEnd)), outsidePressEnabled && mergeCleanups(addEventListener(doc, "click", closeOnPressOutsideCapture, true), addEventListener(doc, "pointerdown", closeOnPressOutsideCapture, true), addEventListener(doc, "pointerup", handlePressEndCapture, true), addEventListener(doc, "pointercancel", handlePressEndCapture, true), addEventListener(doc, "mousedown", closeOnPressOutsideCapture, true), addEventListener(doc, "mouseup", handlePressEndCapture, true), addEventListener(doc, "touchstart", handleTouchStartCapture, true), addEventListener(doc, "touchmove", handleTouchMoveCapture, true), addEventListener(doc, "touchend", handleTouchEndCapture, true))); + const unsubscribe = mergeCleanups(escapeKey2 && mergeCleanups(addEventListener(doc, "keydown", closeOnEscapeKeyDown), addEventListener(doc, "compositionstart", handleCompositionStart), addEventListener(doc, "compositionend", handleCompositionEnd)), outsidePressEnabled && mergeCleanups(addEventListener(doc, "click", closeOnPressOutsideCapture, true), addEventListener(doc, "pointerdown", closeOnPressOutsideCapture, true), addEventListener(doc, "pointerup", handlePressEndCapture, true), addEventListener(doc, "pointercancel", handlePressEndCapture, true), addEventListener(doc, "mousedown", closeOnPressOutsideCapture, true), addEventListener(doc, "mouseup", handlePressEndCapture, true), addEventListener(doc, "touchstart", handleTouchStartCapture, { + capture: true, + passive: true + }), addEventListener(doc, "touchmove", handleTouchMoveCapture, { + capture: true, + passive: true + }), addEventListener(doc, "touchend", handleTouchEndCapture, { + capture: true, + passive: true + }))); return () => { unsubscribe(); - compositionTimeout.clear(); preventedPressSuppressionTimeout.clear(); resetPressStartState(); suppressNextOutsideClickRef.current = false; }; - }, [dataRef, floatingElement, escapeKey2, outsidePressEnabled, outsidePress2, open, enabled, escapeKeyBubbles, outsidePressBubbles, closeOnEscapeKeyDown, clearInsideReactTree, getOutsidePressEventProp, hasBlockingChild, isEventWithinOwnElements, tree, store2, cancelDismissOnEndTimeout]); - React22.useEffect(clearInsideReactTree, [outsidePress2, clearInsideReactTree]); - const reference = React22.useMemo(() => ({ + }, [dataRef, floatingElement, escapeKey2, outsidePressEnabled, outsidePress2, open2, enabled, escapeKeyBubbles, outsidePressBubbles, closeOnEscapeKeyDown, clearInsideReactTree, getOutsidePressEventProp, hasBlockingChild, isEventWithinOwnElements, tree, store2, cancelDismissOnEndTimeout, compositionTimeout]); + const reference = React20.useMemo(() => ({ onKeyDown: closeOnEscapeKeyDown, onPointerDown: closeOnReferencePress, onClick: closeOnReferencePress }), [closeOnEscapeKeyDown, closeOnReferencePress]); - const floating = React22.useMemo(() => ({ + const floating = React20.useMemo(() => ({ onKeyDown: closeOnEscapeKeyDown, // `onMouseDown` may be blocked if `event.preventDefault()` is called in // `onPointerDown`, such as with . @@ -3217,3647 +3390,3677 @@ function useDismiss(context, props = {}) { onTouchEndCapture: markInsideReactTree, onTouchMoveCapture: markInsideReactTree }), [closeOnEscapeKeyDown, markInsideReactTree, markPressStartedInsideReactTree, markInsidePressStartPrevented]); - return React22.useMemo(() => enabled ? { + return React20.useMemo(() => enabled ? { reference, floating, trigger: reference } : {}, [enabled, reference, floating]); } -// node_modules/@base-ui/react/floating-ui-react/hooks/useFloating.mjs -var React29 = __toESM(require_react(), 1); - -// node_modules/@floating-ui/core/dist/floating-ui.core.mjs -function computeCoordsFromPlacement(_ref, placement, rtl) { - let { - reference, - floating - } = _ref; - const sideAxis = getSideAxis(placement); - const alignmentAxis = getAlignmentAxis(placement); - const alignLength = getAxisLength(alignmentAxis); - const side = getSide(placement); - const isVertical = sideAxis === "y"; - const commonX = reference.x + reference.width / 2 - floating.width / 2; - const commonY = reference.y + reference.height / 2 - floating.height / 2; - const commonAlign = reference[alignLength] / 2 - floating[alignLength] / 2; - let coords; - switch (side) { - case "top": - coords = { - x: commonX, - y: reference.y - floating.height - }; - break; - case "bottom": - coords = { - x: commonX, - y: reference.y + reference.height - }; - break; - case "right": - coords = { - x: reference.x + reference.width, - y: commonY - }; - break; - case "left": - coords = { - x: reference.x - floating.width, - y: commonY - }; - break; - default: - coords = { - x: reference.x, - y: reference.y - }; +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/utils/popups/popupHandle.mjs +var BasePopupHandle = class { + /** + * Stores of every root currently using this handle, in attach order. A handle is meant to be used + * by a single mounted root, but roots can transiently overlap (e.g. during an animated route + * transition), so this stack lets `attachStore`'s cleanup restore the previous root instead of + * leaving a still-mounted root uncontrollable when a newer overlapping root detaches first. + */ + attachedStores = []; + /** + * Store of the root that currently controls the handle: the most recently attached one still + * mounted, or `null` when no root is attached. Imperative methods are no-ops while this is `null`. + */ + attachedStoreValue = null; + /** + * Listeners notified when `attachedStore` changes, so detached triggers can follow the store pointer. + */ + storeListeners = /* @__PURE__ */ new Set(); + /** + * Creates a handle backed by the store used while no root is attached. + * + * @param fallbackStore Inert, closed store handed to detached triggers while no root is attached, + * so they can render and register without a mounted root. Triggers register into whichever store + * `store` currently resolves to, so while detached they live in this store's trigger map and + * migrate themselves to the root's store (and back) as it attaches/detaches. + * @param componentName Component name used to prefix dev warnings, e.g. `'Menu'` produces + * `MenuHandle.open()` in warning text. + * @param throwOnMissingTrigger Whether `open(triggerId)` throws when no trigger with that id is + * registered. Anchored popups (Menu, Popover, Tooltip, PreviewCard) need a trigger to anchor to, + * so they throw; Dialog is not anchored and instead opens unassociated with a dev warning. + */ + constructor(fallbackStore, componentName, throwOnMissingTrigger = true) { + this.fallbackStore = fallbackStore; + this.componentName = componentName; + this.throwOnMissingTrigger = throwOnMissingTrigger; } - switch (getAlignment(placement)) { - case "start": - coords[alignmentAxis] -= commonAlign * (rtl && isVertical ? -1 : 1); - break; - case "end": - coords[alignmentAxis] += commonAlign * (rtl && isVertical ? -1 : 1); - break; + get attachedStore() { + return this.attachedStoreValue; } - return coords; -} -async function detectOverflow(state, options) { - var _await$platform$isEle; - if (options === void 0) { - options = {}; + /** + * Store that detached triggers read from: the attached root's store, or an inert fallback store + * used while no root is attached. + * @internal + */ + get store() { + return this.attachedStoreValue ?? this.fallbackStore; } - const { - x, - y, - platform: platform3, - rects, - elements, - strategy - } = state; - const { - boundary = "clippingAncestors", - rootBoundary = "viewport", - elementContext = "floating", - altBoundary = false, - padding = 0 - } = evaluate(options, state); - const paddingObject = getPaddingObject(padding); - const altContext = elementContext === "floating" ? "reference" : "floating"; - const element = elements[altBoundary ? altContext : elementContext]; - const clippingClientRect = rectToClientRect(await platform3.getClippingRect({ - element: ((_await$platform$isEle = await (platform3.isElement == null ? void 0 : platform3.isElement(element))) != null ? _await$platform$isEle : true) ? element : element.contextElement || await (platform3.getDocumentElement == null ? void 0 : platform3.getDocumentElement(elements.floating)), - boundary, - rootBoundary, - strategy - })); - const rect = elementContext === "floating" ? { - x, - y, - width: rects.floating.width, - height: rects.floating.height - } : rects.reference; - const offsetParent = await (platform3.getOffsetParent == null ? void 0 : platform3.getOffsetParent(elements.floating)); - const offsetScale = await (platform3.isElement == null ? void 0 : platform3.isElement(offsetParent)) ? await (platform3.getScale == null ? void 0 : platform3.getScale(offsetParent)) || { - x: 1, - y: 1 - } : { - x: 1, - y: 1 - }; - const elementClientRect = rectToClientRect(platform3.convertOffsetParentRelativeRectToViewportRelativeRect ? await platform3.convertOffsetParentRelativeRectToViewportRelativeRect({ - elements, - rect, - offsetParent, - strategy - }) : rect); - return { - top: (clippingClientRect.top - elementClientRect.top + paddingObject.top) / offsetScale.y, - bottom: (elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom) / offsetScale.y, - left: (clippingClientRect.left - elementClientRect.left + paddingObject.left) / offsetScale.x, - right: (elementClientRect.right - clippingClientRect.right + paddingObject.right) / offsetScale.x - }; -} -var MAX_RESET_COUNT = 50; -var computePosition = async (reference, floating, config) => { - const { - placement = "bottom", - strategy = "absolute", - middleware = [], - platform: platform3 - } = config; - const platformWithDetectOverflow = platform3.detectOverflow ? platform3 : { - ...platform3, - detectOverflow - }; - const rtl = await (platform3.isRTL == null ? void 0 : platform3.isRTL(floating)); - let rects = await platform3.getElementRects({ - reference, - floating, - strategy - }); - let { - x, - y - } = computeCoordsFromPlacement(rects, placement, rtl); - let statefulPlacement = placement; - let resetCount = 0; - const middlewareData = {}; - for (let i = 0; i < middleware.length; i++) { - const currentMiddleware = middleware[i]; - if (!currentMiddleware) { - continue; - } - const { - name, - fn - } = currentMiddleware; - const { - x: nextX, - y: nextY, - data, - reset - } = await fn({ - x, - y, - initialPlacement: placement, - placement: statefulPlacement, - strategy, - middlewareData, - rects, - platform: platformWithDetectOverflow, - elements: { - reference, - floating - } - }); - x = nextX != null ? nextX : x; - y = nextY != null ? nextY : y; - middlewareData[name] = { - ...middlewareData[name], - ...data + /** + * Stable fallback store used for server rendering and hydration. Root stores cannot be recorded on + * the handle during render because a handle can be shared by concurrent server-rendered requests. + * @internal + */ + get serverStore() { + return this.fallbackStore; + } + /** + * Subscribes to changes of the attached store pointer so detached triggers re-render and re-bind + * when a root attaches or detaches. Returns a function that removes the listener. + * @internal + */ + subscribeStore(listener) { + this.storeListeners.add(listener); + return () => { + this.storeListeners.delete(listener); }; - if (reset && resetCount < MAX_RESET_COUNT) { - resetCount++; - if (typeof reset === "object") { - if (reset.placement) { - statefulPlacement = reset.placement; - } - if (reset.rects) { - rects = reset.rects === true ? await platform3.getElementRects({ - reference, - floating, - strategy - }) : reset.rects; - } - ({ - x, - y - } = computeCoordsFromPlacement(rects, statefulPlacement, rtl)); + } + /** + * Points the handle at a root's store and notifies subscribers so detached triggers re-render and + * re-register into it (their registration effect migrates them when the store pointer changes). + * Returns a cleanup function that detaches the store again. + * @internal + */ + attachStore(newStore) { + this.attachedStores.push(newStore); + this.setActiveStore(newStore); + if (true) { + if (this.attachedStores.length > 1) { + const dev = this; + (dev.overlapWarningFrame ??= AnimationFrame.create()).request(() => { + if (this.attachedStores.length > 1) { + console.warn("Base UI: A handle is attached to more than one mounted root at the same time. The most recently mounted root takes over and the previous one stops being controlled by the handle. A handle should be used by a single root that stays mounted for the lifetime of the handle."); + } + }); } - i = -1; } + return () => { + const index2 = this.attachedStores.lastIndexOf(newStore); + if (index2 !== -1) { + this.attachedStores.splice(index2, 1); + } + this.setActiveStore(this.attachedStores[this.attachedStores.length - 1] ?? null); + }; } - return { - x, - y, - placement: statefulPlacement, - strategy, - middlewareData - }; -}; -var flip = function(options) { - if (options === void 0) { - options = {}; + /** + * Sets the store that currently controls the handle and notifies subscribers when it changes, so + * detached triggers re-render and migrate their registration to the new store. + */ + setActiveStore(store2) { + if (this.attachedStoreValue !== store2) { + this.attachedStoreValue = store2; + this.storeListeners.forEach((listener) => { + listener(); + }); + } } - return { - name: "flip", - options, - async fn(state) { - var _middlewareData$arrow, _middlewareData$flip; - const { - placement, - middlewareData, - rects, - initialPlacement, - platform: platform3, - elements - } = state; - const { - mainAxis: checkMainAxis = true, - crossAxis: checkCrossAxis = true, - fallbackPlacements: specifiedFallbackPlacements, - fallbackStrategy = "bestFit", - fallbackAxisSideDirection = "none", - flipAlignment = true, - ...detectOverflowOptions - } = evaluate(options, state); - if ((_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) { - return {}; - } - const side = getSide(placement); - const initialSideAxis = getSideAxis(initialPlacement); - const isBasePlacement = getSide(initialPlacement) === initialPlacement; - const rtl = await (platform3.isRTL == null ? void 0 : platform3.isRTL(elements.floating)); - const fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipAlignment ? [getOppositePlacement(initialPlacement)] : getExpandedPlacements(initialPlacement)); - const hasFallbackAxisSideDirection = fallbackAxisSideDirection !== "none"; - if (!specifiedFallbackPlacements && hasFallbackAxisSideDirection) { - fallbackPlacements.push(...getOppositeAxisPlacements(initialPlacement, flipAlignment, fallbackAxisSideDirection, rtl)); + /** + * Opens the attached root's store and associates it with the trigger with the given id, or a + * no-op (with a dev warning) while no root is attached. Shared by every concrete handle's public + * `open()` method, which only narrows the parameter type. + * + * When a trigger id is given but no matching trigger is registered, anchored popups throw (see + * `throwOnMissingTrigger`); Dialog opens unassociated with a dev warning instead. + * + * This method should only be called in an event handler or an effect (not during rendering). + * + * @param triggerId ID of the trigger to associate with the popup, or `null`/`undefined` to open + * without associating any trigger. + */ + openByTrigger(triggerId) { + const attachedStore = this.attachedStore; + if (attachedStore === null) { + if (true) { + console.warn(`Base UI: ${this.componentName}Handle.open() was called while no root using this handle is mounted. The call was ignored; mount a root with this handle before opening it imperatively.`); } - const placements2 = [initialPlacement, ...fallbackPlacements]; - const overflow = await platform3.detectOverflow(state, detectOverflowOptions); - const overflows = []; - let overflowsData = ((_middlewareData$flip = middlewareData.flip) == null ? void 0 : _middlewareData$flip.overflows) || []; - if (checkMainAxis) { - overflows.push(overflow[side]); + return; + } + let triggerElement; + if (triggerId) { + for (let i = this.attachedStores.length - 1; i >= 0 && !triggerElement; i -= 1) { + triggerElement = this.attachedStores[i].context.triggerElements.getById(triggerId); } - if (checkCrossAxis) { - const sides2 = getAlignmentSides(placement, rects, rtl); - overflows.push(overflow[sides2[0]], overflow[sides2[1]]); + triggerElement ??= this.fallbackStore.context.triggerElements.getById(triggerId); + } + if (triggerId && !triggerElement) { + if (this.throwOnMissingTrigger) { + throw new Error(true ? `Base UI: ${this.componentName}Handle.open() was called with the trigger id "${triggerId}", but no matching trigger is registered with this handle. An anchored popup cannot open without a trigger to anchor to. Pass the id of a mounted ${this.componentName}.Trigger that has this handle set on its "handle" prop.` : formatErrorMessage_default(99, this.componentName, triggerId, this.componentName)); } - overflowsData = [...overflowsData, { - placement, - overflows - }]; - if (!overflows.every((side2) => side2 <= 0)) { - var _middlewareData$flip2, _overflowsData$filter; - const nextIndex = (((_middlewareData$flip2 = middlewareData.flip) == null ? void 0 : _middlewareData$flip2.index) || 0) + 1; - const nextPlacement = placements2[nextIndex]; - if (nextPlacement) { - const ignoreCrossAxisOverflow = checkCrossAxis === "alignment" ? initialSideAxis !== getSideAxis(nextPlacement) : false; - if (!ignoreCrossAxisOverflow || // We leave the current main axis only if every placement on that axis - // overflows the main axis. - overflowsData.every((d) => getSideAxis(d.placement) === initialSideAxis ? d.overflows[0] > 0 : true)) { - return { - data: { - index: nextIndex, - overflows: overflowsData - }, - reset: { - placement: nextPlacement - } - }; - } - } - let resetPlacement = (_overflowsData$filter = overflowsData.filter((d) => d.overflows[0] <= 0).sort((a, b) => a.overflows[1] - b.overflows[1])[0]) == null ? void 0 : _overflowsData$filter.placement; - if (!resetPlacement) { - switch (fallbackStrategy) { - case "bestFit": { - var _overflowsData$filter2; - const placement2 = (_overflowsData$filter2 = overflowsData.filter((d) => { - if (hasFallbackAxisSideDirection) { - const currentSideAxis = getSideAxis(d.placement); - return currentSideAxis === initialSideAxis || // Create a bias to the `y` side axis due to horizontal - // reading directions favoring greater width. - currentSideAxis === "y"; - } - return true; - }).map((d) => [d.placement, d.overflows.filter((overflow2) => overflow2 > 0).reduce((acc, overflow2) => acc + overflow2, 0)]).sort((a, b) => a[1] - b[1])[0]) == null ? void 0 : _overflowsData$filter2[0]; - if (placement2) { - resetPlacement = placement2; - } - break; - } - case "initialPlacement": - resetPlacement = initialPlacement; - break; - } - } - if (placement !== resetPlacement) { - return { - reset: { - placement: resetPlacement - } - }; - } + if (true) { + console.warn(`Base UI: ${this.componentName}Handle.open: No trigger found with id "${triggerId}". The popup will open, but the trigger will not be associated with it.`); } - return {}; } - }; -}; -function getSideOffsets(overflow, rect) { - return { - top: overflow.top - rect.height, - right: overflow.right - rect.width, - bottom: overflow.bottom - rect.height, - left: overflow.left - rect.width - }; -} -function isAnySideFullyClipped(overflow) { - return sides.some((side) => overflow[side] >= 0); -} -var hide = function(options) { - if (options === void 0) { - options = {}; + attachedStore.setOpen(true, createChangeEventDetails(reason_parts_exports.imperativeAction, void 0, triggerElement)); } - return { - name: "hide", - options, - async fn(state) { - const { - rects, - platform: platform3 - } = state; - const { - strategy = "referenceHidden", - ...detectOverflowOptions - } = evaluate(options, state); - switch (strategy) { - case "referenceHidden": { - const overflow = await platform3.detectOverflow(state, { - ...detectOverflowOptions, - elementContext: "reference" - }); - const offsets = getSideOffsets(overflow, rects.reference); - return { - data: { - referenceHiddenOffsets: offsets, - referenceHidden: isAnySideFullyClipped(offsets) - } - }; - } - case "escaped": { - const overflow = await platform3.detectOverflow(state, { - ...detectOverflowOptions, - altBoundary: true - }); - const offsets = getSideOffsets(overflow, rects.floating); - return { - data: { - escapedOffsets: offsets, - escaped: isAnySideFullyClipped(offsets) - } - }; - } - default: { - return {}; - } + /** + * Closes the popup by setting the attached root's store to closed, or a no-op (with a dev warning) + * while no root is attached. Shared by every concrete handle's public `close()` method. + * + * This method should only be called in an event handler or an effect (not during rendering). + */ + closePopup() { + const attachedStore = this.attachedStore; + if (attachedStore === null) { + if (true) { + console.warn(`Base UI: ${this.componentName}Handle.close() was called while no root using this handle is mounted. The call was ignored.`); } + return; } - }; + attachedStore.setOpen(false, createChangeEventDetails(reason_parts_exports.imperativeAction)); + } }; -var originSides = /* @__PURE__ */ new Set(["left", "top"]); -async function convertValueToCoords(state, options) { + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/utils/popups/popupStoreUtils.mjs +var React22 = __toESM(require_react(), 1); +var ReactDOM3 = __toESM(require_react_dom(), 1); + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/floating-ui-react/hooks/useSyncedFloatingRootContext.mjs +function useSyncedFloatingRootContext(options) { const { - placement, - platform: platform3, - elements - } = state; - const rtl = await (platform3.isRTL == null ? void 0 : platform3.isRTL(elements.floating)); - const side = getSide(placement); - const alignment = getAlignment(placement); - const isVertical = getSideAxis(placement) === "y"; - const mainAxisMulti = originSides.has(side) ? -1 : 1; - const crossAxisMulti = rtl && isVertical ? -1 : 1; - const rawValue = evaluate(options, state); - let { - mainAxis, - crossAxis, - alignmentAxis - } = typeof rawValue === "number" ? { - mainAxis: rawValue, - crossAxis: 0, - alignmentAxis: null - } : { - mainAxis: rawValue.mainAxis || 0, - crossAxis: rawValue.crossAxis || 0, - alignmentAxis: rawValue.alignmentAxis - }; - if (alignment && typeof alignmentAxis === "number") { - crossAxis = alignment === "end" ? alignmentAxis * -1 : alignmentAxis; - } - return isVertical ? { - x: crossAxis * crossAxisMulti, - y: mainAxis * mainAxisMulti - } : { - x: mainAxis * mainAxisMulti, - y: crossAxis * crossAxisMulti - }; + popupStore, + treatPopupAsFloatingElement = false, + floatingRootContext: store2, + floatingId, + nested, + onOpenChange + } = options; + const open2 = popupStore.useState("open"); + const referenceElement = popupStore.useState("activeTriggerElement"); + const floatingElement = popupStore.useState(treatPopupAsFloatingElement ? "popupElement" : "positionerElement"); + const handleOpenChange = onOpenChange; + popupStore.useSyncedValue("floatingId", floatingId); + useIsoLayoutEffect(() => { + const valuesToSync = { + open: open2, + floatingId, + referenceElement, + floatingElement + }; + if (isElement(referenceElement)) { + valuesToSync.domReferenceElement = referenceElement; + } + if (store2.state.positionReference === store2.state.referenceElement) { + valuesToSync.positionReference = referenceElement; + } + store2.update(valuesToSync); + }, [open2, floatingId, referenceElement, floatingElement, store2]); + store2.context.onOpenChange = handleOpenChange; + store2.context.nested = nested; + return store2; } -var offset = function(options) { - if (options === void 0) { - options = 0; - } - return { - name: "offset", - options, - async fn(state) { - var _middlewareData$offse, _middlewareData$arrow; - const { - x, - y, - placement, - middlewareData - } = state; - const diffCoords = await convertValueToCoords(state, options); - if (placement === ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse.placement) && (_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) { - return {}; + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/internals/useUnmountAfterClose.mjs +var React21 = __toESM(require_react(), 1); +function useUnmountAfterClose(parameters) { + const { + open: open2, + ref, + preventUnmountOnClose, + setPreventUnmountOnClose: setPreventUnmountOnCloseParam, + onUnmount, + animateInitialOpen + } = parameters; + const setPreventUnmountOnClose = useStableCallback(setPreventUnmountOnCloseParam); + const { + mounted, + setMounted, + transitionStatus + } = useTransitionStatus(open2, false, false, animateInitialOpen); + const preventUnmountingOnClose = open2 ? false : preventUnmountOnClose; + useIsoLayoutEffect(() => { + if (open2) { + setPreventUnmountOnClose(false); + } + }, [open2, setPreventUnmountOnClose]); + const mountedRef = React21.useRef(mounted); + const pendingUnmountRef = React21.useRef(false); + const rerender = useForcedRerendering(); + const unmount = () => { + mountedRef.current = false; + setMounted(false); + onUnmount(); + }; + useIsoLayoutEffect(() => { + mountedRef.current = mounted; + if (pendingUnmountRef.current) { + pendingUnmountRef.current = false; + if (!open2 && mounted) { + unmount(); + } + } + }); + const forceUnmount = useStableCallback(() => { + if (!mountedRef.current) { + return; + } + if (open2) { + pendingUnmountRef.current = true; + rerender(); + return; + } + unmount(); + }); + useOpenChangeComplete({ + enabled: mounted && !open2 && !preventUnmountingOnClose, + open: open2, + ref, + onComplete() { + if (!open2) { + forceUnmount(); } - return { - x: x + diffCoords.x, - y: y + diffCoords.y, - data: { - ...diffCoords, - placement - } - }; } + }); + return { + mounted, + transitionStatus, + preventUnmountingOnClose, + forceUnmount }; +} + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/utils/popups/popupStoreUtils.mjs +var FOCUSABLE_POPUP_PROPS = { + tabIndex: -1, + [FOCUSABLE_ATTRIBUTE]: "" }; -var shift = function(options) { - if (options === void 0) { - options = {}; +function usePopupRootStore(createStore, treatPopupAsFloatingElement = false) { + const floatingId = useId(); + const nested = useFloatingParentNodeId() != null; + const store2 = useRefWithInit(() => createStore(floatingId, nested)).current; + useSyncedFloatingRootContext({ + popupStore: store2, + treatPopupAsFloatingElement, + floatingRootContext: store2.state.floatingRootContext, + floatingId, + nested, + onOpenChange: store2.setOpen + }); + return store2; +} +function PopupHandleAttachment({ + handle, + store: store2 +}) { + useIsoLayoutEffect(() => { + return handle.attachStore(store2); + }, [handle, store2]); + return null; +} +function syncTriggerCount(store2) { + const triggerCount = store2.context.triggerElements.size; + if (store2.select("open") && store2.state.triggerCount !== triggerCount) { + store2.set("triggerCount", triggerCount); } - return { - name: "shift", - options, - async fn(state) { - const { - x, - y, - placement, - platform: platform3 - } = state; - const { - mainAxis: checkMainAxis = true, - crossAxis: checkCrossAxis = false, - limiter = { - fn: (_ref) => { - let { - x: x2, - y: y2 - } = _ref; - return { - x: x2, - y: y2 - }; - } - }, - ...detectOverflowOptions - } = evaluate(options, state); - const coords = { - x, - y - }; - const overflow = await platform3.detectOverflow(state, detectOverflowOptions); - const crossAxis = getSideAxis(getSide(placement)); - const mainAxis = getOppositeAxis(crossAxis); - let mainAxisCoord = coords[mainAxis]; - let crossAxisCoord = coords[crossAxis]; - if (checkMainAxis) { - const minSide = mainAxis === "y" ? "top" : "left"; - const maxSide = mainAxis === "y" ? "bottom" : "right"; - const min2 = mainAxisCoord + overflow[minSide]; - const max2 = mainAxisCoord - overflow[maxSide]; - mainAxisCoord = clamp(min2, mainAxisCoord, max2); +} +function useTriggerRegistration(id, store2) { + const registrationRef = React22.useRef(null); + return useStableCallback((element) => { + const registration = registrationRef.current; + if (registration !== null) { + if (registration.element === element && registration.store === store2 && registration.id === id) { + return; } - if (checkCrossAxis) { - const minSide = crossAxis === "y" ? "top" : "left"; - const maxSide = crossAxis === "y" ? "bottom" : "right"; - const min2 = crossAxisCoord + overflow[minSide]; - const max2 = crossAxisCoord - overflow[maxSide]; - crossAxisCoord = clamp(min2, crossAxisCoord, max2); + registrationRef.current = null; + const registeredStore = registration.store; + if (registeredStore.context.triggerElements.getById(registration.id) === registration.element) { + registeredStore.context.triggerElements.delete(registration.id); + syncTriggerCount(registeredStore); } - const limitedCoords = limiter.fn({ - ...state, - [mainAxis]: mainAxisCoord, - [crossAxis]: crossAxisCoord - }); - return { - ...limitedCoords, - data: { - x: limitedCoords.x - x, - y: limitedCoords.y - y, - enabled: { - [mainAxis]: checkMainAxis, - [crossAxis]: checkCrossAxis - } - } + } + if (element !== null && id !== void 0) { + registrationRef.current = { + store: store2, + id, + element }; + store2.context.triggerElements.add(id, element); + syncTriggerCount(store2); } - }; -}; -var limitShift = function(options) { - if (options === void 0) { - options = {}; + }); +} +function createPopupOpenState(state, open2, trigger, preventUnmountOnClose = false) { + let preventUnmountingOnClose = state.preventUnmountingOnClose; + if (open2) { + preventUnmountingOnClose = false; + } else if (preventUnmountOnClose) { + preventUnmountingOnClose = true; + } + const triggerId = trigger?.id ?? null; + let activeTriggerId = state.activeTriggerId; + let activeTriggerElement = state.activeTriggerElement; + if (triggerId || open2) { + activeTriggerId = triggerId; + activeTriggerElement = trigger ?? null; } return { - options, - fn(state) { - const { - x, - y, - placement, - rects, - middlewareData - } = state; - const { - offset: offset4 = 0, - mainAxis: checkMainAxis = true, - crossAxis: checkCrossAxis = true - } = evaluate(options, state); - const coords = { - x, - y - }; - const crossAxis = getSideAxis(placement); - const mainAxis = getOppositeAxis(crossAxis); - let mainAxisCoord = coords[mainAxis]; - let crossAxisCoord = coords[crossAxis]; - const rawOffset = evaluate(offset4, state); - const computedOffset = typeof rawOffset === "number" ? { - mainAxis: rawOffset, - crossAxis: 0 - } : { - mainAxis: 0, - crossAxis: 0, - ...rawOffset - }; - if (checkMainAxis) { - const len = mainAxis === "y" ? "height" : "width"; - const limitMin = rects.reference[mainAxis] - rects.floating[len] + computedOffset.mainAxis; - const limitMax = rects.reference[mainAxis] + rects.reference[len] - computedOffset.mainAxis; - if (mainAxisCoord < limitMin) { - mainAxisCoord = limitMin; - } else if (mainAxisCoord > limitMax) { - mainAxisCoord = limitMax; - } - } - if (checkCrossAxis) { - var _middlewareData$offse, _middlewareData$offse2; - const len = mainAxis === "y" ? "width" : "height"; - const isOriginSide = originSides.has(getSide(placement)); - const limitMin = rects.reference[crossAxis] - rects.floating[len] + (isOriginSide ? ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse[crossAxis]) || 0 : 0) + (isOriginSide ? 0 : computedOffset.crossAxis); - const limitMax = rects.reference[crossAxis] + rects.reference[len] + (isOriginSide ? 0 : ((_middlewareData$offse2 = middlewareData.offset) == null ? void 0 : _middlewareData$offse2[crossAxis]) || 0) - (isOriginSide ? computedOffset.crossAxis : 0); - if (crossAxisCoord < limitMin) { - crossAxisCoord = limitMin; - } else if (crossAxisCoord > limitMax) { - crossAxisCoord = limitMax; - } - } - return { - [mainAxis]: mainAxisCoord, - [crossAxis]: crossAxisCoord - }; + open: open2, + preventUnmountingOnClose, + activeTriggerId, + activeTriggerElement, + // An open request without a trigger (a handle's `open(null)` or `openWithPayload()`) must not + // be reassociated with a lone registered trigger later on. Controlled and default opens never + // pass through here, so they keep claiming a lone trigger. A close request keeps the flag: a + // controlled root may decline it and stay open, so the Root clears the flag only once the + // popup is effectively closed. + openedWithoutTrigger: open2 ? trigger == null : state.openedWithoutTrigger + }; +} +function attachPreventUnmountOnClose(eventDetails) { + let preventUnmountOnClose = false; + eventDetails.preventUnmountOnClose = () => { + preventUnmountOnClose = true; + }; + return () => preventUnmountOnClose; +} +function applyPopupOpenChange(store2, nextOpen, eventDetails, options = {}) { + const reason = eventDetails.reason; + const isHover = reason === reason_parts_exports.triggerHover; + const isFocusOpen = nextOpen && reason === reason_parts_exports.triggerFocus; + const isDismissClose = !nextOpen && (reason === reason_parts_exports.triggerPress || reason === reason_parts_exports.escapeKey); + const shouldPreventUnmountOnClose = attachPreventUnmountOnClose(eventDetails); + store2.context.onOpenChange?.(nextOpen, eventDetails); + if (eventDetails.isCanceled) { + return; + } + options.onBeforeDispatch?.(); + store2.state.floatingRootContext.dispatchOpenChange(nextOpen, eventDetails); + const changeState = () => { + const popupOpenState = createPopupOpenState(store2.state, nextOpen, eventDetails.trigger, shouldPreventUnmountOnClose()); + const updatedState = { + ...options.extraState, + ...popupOpenState + }; + if (isFocusOpen) { + updatedState.instantType = "focus"; + } else if (isDismissClose) { + updatedState.instantType = "dismiss"; + } else if (isHover) { + updatedState.instantType = void 0; } + store2.update(updatedState); }; -}; -var size = function(options) { - if (options === void 0) { - options = {}; + if (isHover) { + ReactDOM3.flushSync(changeState); + } else { + changeState(); } +} +function useTriggerDataForwarding(triggerId, triggerElementRef, store2, stateUpdates) { + const isMountedByThisTrigger = store2.useState("isMountedByTrigger", triggerId); + const baseRegisterTrigger = useTriggerRegistration(triggerId, store2); + const applyTriggerData = useStableCallback((element) => { + const open2 = store2.select("open"); + const activeTriggerId = store2.select("activeTriggerId"); + if (activeTriggerId === triggerId) { + const changes = { + activeTriggerElement: element, + ...open2 ? stateUpdates : null + }; + store2.update(changes); + return; + } + if (activeTriggerId == null && open2 && !store2.state.openedWithoutTrigger) { + const changes = { + activeTriggerId: triggerId ?? null, + activeTriggerElement: element, + ...stateUpdates + }; + store2.update(changes); + } + }); + const registerTrigger = useStableCallback((element) => { + baseRegisterTrigger(element); + if (element) { + applyTriggerData(element); + } + }); + useIsoLayoutEffect(() => { + registerTrigger(triggerElementRef.current); + return () => registerTrigger(null); + }, [registerTrigger, triggerElementRef, store2, triggerId]); + useIsoLayoutEffect(() => { + if (isMountedByThisTrigger) { + const changes = { + activeTriggerElement: triggerElementRef.current, + ...stateUpdates + }; + store2.update(changes); + } + }, [isMountedByThisTrigger, store2, triggerElementRef, ...Object.values(stateUpdates)]); return { - name: "size", - options, - async fn(state) { - var _state$middlewareData, _state$middlewareData2; - const { - placement, - rects, - platform: platform3, - elements - } = state; - const { - apply = () => { - }, - ...detectOverflowOptions - } = evaluate(options, state); - const overflow = await platform3.detectOverflow(state, detectOverflowOptions); - const side = getSide(placement); - const alignment = getAlignment(placement); - const isYAxis = getSideAxis(placement) === "y"; - const { - width, - height - } = rects.floating; - let heightSide; - let widthSide; - if (side === "top" || side === "bottom") { - heightSide = side; - widthSide = alignment === (await (platform3.isRTL == null ? void 0 : platform3.isRTL(elements.floating)) ? "start" : "end") ? "left" : "right"; - } else { - widthSide = side; - heightSide = alignment === "end" ? "top" : "bottom"; - } - const maximumClippingHeight = height - overflow.top - overflow.bottom; - const maximumClippingWidth = width - overflow.left - overflow.right; - const overflowAvailableHeight = min(height - overflow[heightSide], maximumClippingHeight); - const overflowAvailableWidth = min(width - overflow[widthSide], maximumClippingWidth); - const noShift = !state.middlewareData.shift; - let availableHeight = overflowAvailableHeight; - let availableWidth = overflowAvailableWidth; - if ((_state$middlewareData = state.middlewareData.shift) != null && _state$middlewareData.enabled.x) { - availableWidth = maximumClippingWidth; + registerTrigger, + isMountedByThisTrigger + }; +} +function useImplicitActiveTrigger(store2, options = {}) { + const { + closeOnActiveTriggerUnmount = false + } = options; + const resolvedActiveTriggerIdRef = React22.useRef(null); + const open2 = store2.useState("open"); + const reactiveTriggerCount = store2.useState("triggerCount"); + const activeTriggerId = store2.useState("activeTriggerId"); + const reactiveActiveTriggerElement = store2.useState("activeTriggerElement"); + useIsoLayoutEffect(() => { + if (!open2) { + resolvedActiveTriggerIdRef.current = null; + if (store2.state.triggerCount !== 0) { + store2.set("triggerCount", 0); } - if ((_state$middlewareData2 = state.middlewareData.shift) != null && _state$middlewareData2.enabled.y) { - availableHeight = maximumClippingHeight; + if (store2.state.openedWithoutTrigger) { + store2.set("openedWithoutTrigger", false); } - if (noShift && !alignment) { - const xMin = max(overflow.left, 0); - const xMax = max(overflow.right, 0); - const yMin = max(overflow.top, 0); - const yMax = max(overflow.bottom, 0); - if (isYAxis) { - availableWidth = width - 2 * (xMin !== 0 || xMax !== 0 ? xMin + xMax : max(overflow.left, overflow.right)); - } else { - availableHeight = height - 2 * (yMin !== 0 || yMax !== 0 ? yMin + yMax : max(overflow.top, overflow.bottom)); + return; + } + const triggerCount = store2.context.triggerElements.size; + const stateUpdates = {}; + if (store2.state.triggerCount !== triggerCount) { + stateUpdates.triggerCount = triggerCount; + } + const currentActiveTriggerId = store2.select("activeTriggerId"); + let lostActiveTriggerId = null; + if (currentActiveTriggerId) { + const activeTriggerElement = store2.context.triggerElements.getById(currentActiveTriggerId); + if (!activeTriggerElement) { + for (const [triggerId, triggerElement] of store2.context.triggerElements.entries()) { + if (triggerElement === store2.state.activeTriggerElement) { + stateUpdates.activeTriggerId = triggerId; + stateUpdates.activeTriggerElement = triggerElement; + resolvedActiveTriggerIdRef.current = triggerId; + break; + } + } + if (stateUpdates.activeTriggerId === void 0) { + if (resolvedActiveTriggerIdRef.current === currentActiveTriggerId) { + lostActiveTriggerId = currentActiveTriggerId; + } else { + resolvedActiveTriggerIdRef.current = null; + } + } + } else { + resolvedActiveTriggerIdRef.current = currentActiveTriggerId; + if (activeTriggerElement !== store2.state.activeTriggerElement) { + stateUpdates.activeTriggerElement = activeTriggerElement; } } - await apply({ - ...state, - availableWidth, - availableHeight - }); - const nextDimensions = await platform3.getDimensions(elements.floating); - if (width !== nextDimensions.width || height !== nextDimensions.height) { - return { - reset: { - rects: true + } else { + resolvedActiveTriggerIdRef.current = null; + } + if (!lostActiveTriggerId && !currentActiveTriggerId && !store2.state.openedWithoutTrigger && triggerCount === 1) { + const iteratorResult = store2.context.triggerElements.entries().next(); + if (!iteratorResult.done) { + const [implicitTriggerId, implicitTriggerElement] = iteratorResult.value; + stateUpdates.activeTriggerId = implicitTriggerId; + stateUpdates.activeTriggerElement = implicitTriggerElement; + resolvedActiveTriggerIdRef.current = implicitTriggerId; + } + } + if (stateUpdates.triggerCount !== void 0 || stateUpdates.activeTriggerId !== void 0 || stateUpdates.activeTriggerElement !== void 0) { + store2.update(stateUpdates); + } + if (lostActiveTriggerId) { + if (closeOnActiveTriggerUnmount) { + queueMicrotask(() => { + if (store2.select("open") && store2.select("activeTriggerId") === lostActiveTriggerId && !store2.context.triggerElements.getById(lostActiveTriggerId)) { + const eventDetails = createChangeEventDetails(reason_parts_exports.none); + store2.setOpen(false, eventDetails); + if (!eventDetails.isCanceled) { + store2.update({ + activeTriggerId: null, + activeTriggerElement: null + }); + } } - }; + }); } - return {}; } - }; -}; - -// node_modules/@floating-ui/dom/dist/floating-ui.dom.mjs -function getCssDimensions(element) { - const css = getComputedStyle2(element); - let width = parseFloat(css.width) || 0; - let height = parseFloat(css.height) || 0; - const hasOffset = isHTMLElement(element); - const offsetWidth = hasOffset ? element.offsetWidth : width; - const offsetHeight = hasOffset ? element.offsetHeight : height; - const shouldFallback = round(width) !== offsetWidth || round(height) !== offsetHeight; - if (shouldFallback) { - width = offsetWidth; - height = offsetHeight; - } - return { - width, - height, - $: shouldFallback - }; + }, [open2, store2, reactiveTriggerCount, activeTriggerId, reactiveActiveTriggerElement, closeOnActiveTriggerUnmount]); } -function unwrapElement(element) { - return !isElement(element) ? element.contextElement : element; -} -function getScale(element) { - const domElement = unwrapElement(element); - if (!isHTMLElement(domElement)) { - return createCoords(1); - } - const rect = domElement.getBoundingClientRect(); +function useOpenStateTransitions(open2, store2, onUnmount, animateInitialOpen) { const { - width, - height, - $ - } = getCssDimensions(domElement); - let x = ($ ? round(rect.width) : rect.width) / width; - let y = ($ ? round(rect.height) : rect.height) / height; - if (!x || !Number.isFinite(x)) { - x = 1; - } - if (!y || !Number.isFinite(y)) { - y = 1; - } + mounted, + transitionStatus, + forceUnmount + } = useUnmountAfterClose({ + open: open2, + ref: store2.context.popupRef, + preventUnmountOnClose: store2.useState("preventUnmountingOnClose"), + setPreventUnmountOnClose: (preventUnmountOnClose) => store2.set("preventUnmountingOnClose", preventUnmountOnClose), + animateInitialOpen, + onUnmount() { + store2.update({ + activeTriggerId: null, + activeTriggerElement: null, + mounted: false, + preventUnmountingOnClose: false + }); + onUnmount?.(); + store2.context.onOpenChangeComplete?.(false); + } + }); + useRefWithInit(() => { + store2.set("mounted", mounted); + return null; + }); + store2.useSyncedValues({ + mounted, + transitionStatus + }); return { - x, - y + forceUnmount, + transitionStatus }; } -var noOffsets = /* @__PURE__ */ createCoords(0); -function getVisualOffsets(element) { - const win = getWindow(element); - if (!isWebKit() || !win.visualViewport) { - return noOffsets; +function usePopupInteractionProps(store2, statePart) { + store2.useSyncedValues(statePart); + useIsoLayoutEffect(() => () => { + store2.update({ + activeTriggerProps: EMPTY_OBJECT, + inactiveTriggerProps: EMPTY_OBJECT, + popupProps: EMPTY_OBJECT + }); + }, [store2]); +} + +// node_modules/.store/@base-ui/react@1.9.0-uHNeM94OdjIeOtpyYGvL0A/node_modules/@base-ui/react/utils/popups/popupTriggerMap.mjs +var devElementIdsByMap; +function getDevElementIds(map) { + devElementIdsByMap ??= /* @__PURE__ */ new WeakMap(); + let elementIds = devElementIdsByMap.get(map); + if (!elementIds) { + elementIds = /* @__PURE__ */ new WeakMap(); + devElementIdsByMap.set(map, elementIds); } - return { - x: win.visualViewport.offsetLeft, - y: win.visualViewport.offsetTop - }; -} -function shouldAddVisualOffsets(element, isFixed, floatingOffsetParent) { - if (isFixed === void 0) { - isFixed = false; - } - if (!floatingOffsetParent || isFixed && floatingOffsetParent !== getWindow(element)) { - return false; - } - return isFixed; + return elementIds; } -function getBoundingClientRect(element, includeScale, isFixedStrategy, offsetParent) { - if (includeScale === void 0) { - includeScale = false; +var PopupTriggerMap = class { + idMap = /* @__PURE__ */ new Map(); + /** + * Adds a trigger element with the given ID. + * + * Note: The provided element is assumed to not be registered under multiple IDs. + */ + add(id, element) { + if (true) { + const elementIds = getDevElementIds(this); + const existingId = elementIds.get(element); + if (existingId !== void 0 && existingId !== id) { + throw new Error("Base UI: A trigger element cannot be registered under multiple IDs in PopupTriggerMap."); + } + const previousElement = this.idMap.get(id); + if (previousElement !== void 0 && previousElement !== element) { + elementIds.delete(previousElement); + } + elementIds.set(element, id); + } + this.idMap.set(id, element); } - if (isFixedStrategy === void 0) { - isFixedStrategy = false; + /** + * Removes the trigger element with the given ID. + */ + delete(id) { + if (true) { + const element = this.idMap.get(id); + if (element !== void 0) { + devElementIdsByMap?.get(this)?.delete(element); + } + } + this.idMap.delete(id); } - const clientRect = element.getBoundingClientRect(); - const domElement = unwrapElement(element); - let scale = createCoords(1); - if (includeScale) { - if (offsetParent) { - if (isElement(offsetParent)) { - scale = getScale(offsetParent); + /** + * Whether the given element is registered as a trigger. + */ + hasElement(element) { + for (const registered of this.idMap.values()) { + if (registered === element) { + return true; } - } else { - scale = getScale(element); } + return false; } - const visualOffsets = shouldAddVisualOffsets(domElement, isFixedStrategy, offsetParent) ? getVisualOffsets(domElement) : createCoords(0); - let x = (clientRect.left + visualOffsets.x) / scale.x; - let y = (clientRect.top + visualOffsets.y) / scale.y; - let width = clientRect.width / scale.x; - let height = clientRect.height / scale.y; - if (domElement) { - const win = getWindow(domElement); - const offsetWin = offsetParent && isElement(offsetParent) ? getWindow(offsetParent) : offsetParent; - let currentWin = win; - let currentIFrame = getFrameElement(currentWin); - while (currentIFrame && offsetParent && offsetWin !== currentWin) { - const iframeScale = getScale(currentIFrame); - const iframeRect = currentIFrame.getBoundingClientRect(); - const css = getComputedStyle2(currentIFrame); - const left = iframeRect.left + (currentIFrame.clientLeft + parseFloat(css.paddingLeft)) * iframeScale.x; - const top = iframeRect.top + (currentIFrame.clientTop + parseFloat(css.paddingTop)) * iframeScale.y; - x *= iframeScale.x; - y *= iframeScale.y; - width *= iframeScale.x; - height *= iframeScale.y; - x += left; - y += top; - currentWin = getWindow(currentIFrame); - currentIFrame = getFrameElement(currentWin); + /** + * Whether there is a registered trigger element matching the given predicate. + */ + hasMatchingElement(predicate) { + for (const element of this.idMap.values()) { + if (predicate(element)) { + return true; + } } + return false; } - return rectToClientRect({ - width, - height, - x, - y - }); -} -function getWindowScrollBarX(element, rect) { - const leftScroll = getNodeScroll(element).scrollLeft; - if (!rect) { - return getBoundingClientRect(getDocumentElement(element)).left + leftScroll; + /** + * Returns the trigger element associated with the given ID, or undefined if no such element exists. + */ + getById(id) { + return this.idMap.get(id); } - return rect.left + leftScroll; + /** + * Returns an iterable of all registered trigger entries, where each entry is a tuple of [id, element]. + */ + entries() { + return this.idMap.entries(); + } + /** + * Returns an iterable of all registered trigger elements. + */ + elements() { + return this.idMap.values(); + } + /** + * Returns the number of registered trigger elements. + */ + get size() { + return this.idMap.size; + } +}; + +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/store/useStore.mjs +var React24 = __toESM(require_react(), 1); +var import_shim = __toESM(require_shim(), 1); +var import_with_selector = __toESM(require_with_selector(), 1); + +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/fastHooks.mjs +var React23 = __toESM(require_react(), 1); +var hooks = []; +var currentInstance = void 0; +function getInstance() { + return currentInstance; } -function getHTMLOffset(documentElement, scroll) { - const htmlRect = documentElement.getBoundingClientRect(); - const x = htmlRect.left + scroll.scrollLeft - getWindowScrollBarX(documentElement, htmlRect); - const y = htmlRect.top + scroll.scrollTop; - return { - x, - y - }; +function register(hook) { + hooks.push(hook); } -function convertOffsetParentRelativeRectToViewportRelativeRect(_ref) { - let { - elements, - rect, - offsetParent, - strategy - } = _ref; - const isFixed = strategy === "fixed"; - const documentElement = getDocumentElement(offsetParent); - const topLayer = elements ? isTopLayer(elements.floating) : false; - if (offsetParent === documentElement || topLayer && isFixed) { - return rect; - } - let scroll = { - scrollLeft: 0, - scrollTop: 0 - }; - let scale = createCoords(1); - const offsets = createCoords(0); - const isOffsetParentAnElement = isHTMLElement(offsetParent); - if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { - if (getNodeName(offsetParent) !== "body" || isOverflowElement(documentElement)) { - scroll = getNodeScroll(offsetParent); - } - if (isOffsetParentAnElement) { - const offsetRect = getBoundingClientRect(offsetParent); - scale = getScale(offsetParent); - offsets.x = offsetRect.x + offsetParent.clientLeft; - offsets.y = offsetRect.y + offsetParent.clientTop; +function fastComponent(fn) { + const FastComponent = (props, forwardedRef) => { + const instance = useRefWithInit(createInstance).current; + let result; + try { + currentInstance = instance; + for (const hook of hooks) { + hook.before(instance); + } + result = fn(props, forwardedRef); + for (const hook of hooks) { + hook.after(instance); + } + instance.didInitialize = true; + } finally { + currentInstance = void 0; } - } - const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll) : createCoords(0); - return { - width: rect.width * scale.x, - height: rect.height * scale.y, - x: rect.x * scale.x - scroll.scrollLeft * scale.x + offsets.x + htmlOffset.x, - y: rect.y * scale.y - scroll.scrollTop * scale.y + offsets.y + htmlOffset.y + return result; }; + FastComponent.displayName = fn.displayName || fn.name; + return FastComponent; } -function getClientRects(element) { - return Array.from(element.getClientRects()); +function fastComponentRef(fn) { + return /* @__PURE__ */ React23.forwardRef(fastComponent(fn)); } -function getDocumentRect(element) { - const html = getDocumentElement(element); - const scroll = getNodeScroll(element); - const body = element.ownerDocument.body; - const width = max(html.scrollWidth, html.clientWidth, body.scrollWidth, body.clientWidth); - const height = max(html.scrollHeight, html.clientHeight, body.scrollHeight, body.clientHeight); - let x = -scroll.scrollLeft + getWindowScrollBarX(element); - const y = -scroll.scrollTop; - if (getComputedStyle2(body).direction === "rtl") { - x += max(html.clientWidth, body.clientWidth) - width; - } +function createInstance() { return { - width, - height, - x, - y + didInitialize: false }; } -var SCROLLBAR_MAX = 25; -function getViewportRect(element, strategy) { - const win = getWindow(element); - const html = getDocumentElement(element); - const visualViewport = win.visualViewport; - let width = html.clientWidth; - let height = html.clientHeight; - let x = 0; - let y = 0; - if (visualViewport) { - width = visualViewport.width; - height = visualViewport.height; - const visualViewportBased = isWebKit(); - if (!visualViewportBased || visualViewportBased && strategy === "fixed") { - x = visualViewport.offsetLeft; - y = visualViewport.offsetTop; - } - } - const windowScrollbarX = getWindowScrollBarX(html); - if (windowScrollbarX <= 0) { - const doc = html.ownerDocument; - const body = doc.body; - const bodyStyles = getComputedStyle(body); - const bodyMarginInline = doc.compatMode === "CSS1Compat" ? parseFloat(bodyStyles.marginLeft) + parseFloat(bodyStyles.marginRight) || 0 : 0; - const clippingStableScrollbarWidth = Math.abs(html.clientWidth - body.clientWidth - bodyMarginInline); - if (clippingStableScrollbarWidth <= SCROLLBAR_MAX) { - width -= clippingStableScrollbarWidth; - } - } else if (windowScrollbarX <= SCROLLBAR_MAX) { - width += windowScrollbarX; - } - return { - width, - height, - x, - y - }; + +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/store/useStore.mjs +var canUseRawUseSyncExternalStore = isReactVersionAtLeast(19); +var useStoreImplementation = canUseRawUseSyncExternalStore ? useStoreFast : useStoreLegacy; +function useStore(store2, selector, a1, a2, a3) { + return useStoreImplementation(store2, selector, a1, a2, a3); } -function getInnerBoundingClientRect(element, strategy) { - const clientRect = getBoundingClientRect(element, true, strategy === "fixed"); - const top = clientRect.top + element.clientTop; - const left = clientRect.left + element.clientLeft; - const scale = isHTMLElement(element) ? getScale(element) : createCoords(1); - const width = element.clientWidth * scale.x; - const height = element.clientHeight * scale.y; - const x = left * scale.x; - const y = top * scale.y; - return { - width, - height, - x, - y - }; +function useStoreR19(store2, selector, a1, a2, a3) { + const getSelection = React24.useCallback(() => selector(store2.getSnapshot(), a1, a2, a3), [store2, selector, a1, a2, a3]); + return (0, import_shim.useSyncExternalStore)(store2.subscribe, getSelection, getSelection); } -function getClientRectFromClippingAncestor(element, clippingAncestor, strategy) { - let rect; - if (clippingAncestor === "viewport") { - rect = getViewportRect(element, strategy); - } else if (clippingAncestor === "document") { - rect = getDocumentRect(getDocumentElement(element)); - } else if (isElement(clippingAncestor)) { - rect = getInnerBoundingClientRect(clippingAncestor, strategy); - } else { - const visualOffsets = getVisualOffsets(element); - rect = { - x: clippingAncestor.x - visualOffsets.x, - y: clippingAncestor.y - visualOffsets.y, - width: clippingAncestor.width, - height: clippingAncestor.height - }; - } - return rectToClientRect(rect); -} -function hasFixedPositionAncestor(element, stopNode) { - const parentNode = getParentNode(element); - if (parentNode === stopNode || !isElement(parentNode) || isLastTraversableNode(parentNode)) { - return false; - } - return getComputedStyle2(parentNode).position === "fixed" || hasFixedPositionAncestor(parentNode, stopNode); -} -function getClippingElementAncestors(element, cache) { - const cachedResult = cache.get(element); - if (cachedResult) { - return cachedResult; - } - let result = getOverflowAncestors(element, [], false).filter((el) => isElement(el) && getNodeName(el) !== "body"); - let currentContainingBlockComputedStyle = null; - const elementIsFixed = getComputedStyle2(element).position === "fixed"; - let currentNode = elementIsFixed ? getParentNode(element) : element; - while (isElement(currentNode) && !isLastTraversableNode(currentNode)) { - const computedStyle = getComputedStyle2(currentNode); - const currentNodeIsContaining = isContainingBlock(currentNode); - if (!currentNodeIsContaining && computedStyle.position === "fixed") { - currentContainingBlockComputedStyle = null; +register({ + before(instance) { + instance.syncIndex = 0; + if (!instance.didInitialize) { + instance.syncTick = 1; + instance.syncHooks = []; + instance.didChangeStore = true; + instance.getSnapshot = () => { + let didChange2 = false; + for (let i = 0; i < instance.syncHooks.length; i += 1) { + const hook = instance.syncHooks[i]; + const value = hook.selector(hook.store.state, hook.a1, hook.a2, hook.a3); + if (!Object.is(hook.value, value)) { + didChange2 = true; + hook.value = value; + } + } + if (didChange2) { + instance.syncTick += 1; + } + return instance.syncTick; + }; } - const shouldDropCurrentNode = elementIsFixed ? !currentNodeIsContaining && !currentContainingBlockComputedStyle : !currentNodeIsContaining && computedStyle.position === "static" && !!currentContainingBlockComputedStyle && (currentContainingBlockComputedStyle.position === "absolute" || currentContainingBlockComputedStyle.position === "fixed") || isOverflowElement(currentNode) && !currentNodeIsContaining && hasFixedPositionAncestor(element, currentNode); - if (shouldDropCurrentNode) { - result = result.filter((ancestor) => ancestor !== currentNode); - } else { - currentContainingBlockComputedStyle = computedStyle; + }, + after(instance) { + if (instance.syncHooks.length > 0) { + if (instance.didChangeStore) { + instance.didChangeStore = false; + instance.subscribe = (onStoreChange) => { + const stores = /* @__PURE__ */ new Set(); + for (const hook of instance.syncHooks) { + stores.add(hook.store); + } + const unsubscribes = []; + for (const store2 of stores) { + unsubscribes.push(store2.subscribe(onStoreChange)); + } + return () => { + for (const unsubscribe of unsubscribes) { + unsubscribe(); + } + }; + }; + } + (0, import_shim.useSyncExternalStore)(instance.subscribe, instance.getSnapshot, instance.getSnapshot); } - currentNode = getParentNode(currentNode); - } - cache.set(element, result); - return result; -} -function getClippingRect(_ref) { - let { - element, - boundary, - rootBoundary, - strategy - } = _ref; - const elementClippingAncestors = boundary === "clippingAncestors" ? isTopLayer(element) ? [] : getClippingElementAncestors(element, this._c) : [].concat(boundary); - const clippingAncestors = [...elementClippingAncestors, rootBoundary]; - const firstRect = getClientRectFromClippingAncestor(element, clippingAncestors[0], strategy); - let top = firstRect.top; - let right = firstRect.right; - let bottom = firstRect.bottom; - let left = firstRect.left; - for (let i = 1; i < clippingAncestors.length; i++) { - const rect = getClientRectFromClippingAncestor(element, clippingAncestors[i], strategy); - top = max(rect.top, top); - right = min(rect.right, right); - bottom = min(rect.bottom, bottom); - left = max(rect.left, left); } - return { - width: right - left, - height: bottom - top, - x: left, - y: top - }; -} -function getDimensions(element) { - const { - width, - height - } = getCssDimensions(element); - return { - width, - height - }; -} -function getRectRelativeToOffsetParent(element, offsetParent, strategy) { - const isOffsetParentAnElement = isHTMLElement(offsetParent); - const documentElement = getDocumentElement(offsetParent); - const isFixed = strategy === "fixed"; - const rect = getBoundingClientRect(element, true, isFixed, offsetParent); - let scroll = { - scrollLeft: 0, - scrollTop: 0 - }; - const offsets = createCoords(0); - function setLeftRTLScrollbarOffset() { - offsets.x = getWindowScrollBarX(documentElement); +}); +function useStoreFast(store2, selector, a1, a2, a3) { + const instance = getInstance(); + if (!instance) { + return useStoreR19(store2, selector, a1, a2, a3); } - if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { - if (getNodeName(offsetParent) !== "body" || isOverflowElement(documentElement)) { - scroll = getNodeScroll(offsetParent); - } - if (isOffsetParentAnElement) { - const offsetRect = getBoundingClientRect(offsetParent, true, isFixed, offsetParent); - offsets.x = offsetRect.x + offsetParent.clientLeft; - offsets.y = offsetRect.y + offsetParent.clientTop; - } else if (documentElement) { - setLeftRTLScrollbarOffset(); + const index2 = instance.syncIndex; + instance.syncIndex += 1; + let hook; + if (!instance.didInitialize) { + hook = { + store: store2, + selector, + a1, + a2, + a3, + value: selector(store2.getSnapshot(), a1, a2, a3) + }; + instance.syncHooks.push(hook); + } else { + hook = instance.syncHooks[index2]; + if (hook.store !== store2 || hook.selector !== selector || !Object.is(hook.a1, a1) || !Object.is(hook.a2, a2) || !Object.is(hook.a3, a3)) { + if (hook.store !== store2) { + instance.didChangeStore = true; + } + hook.store = store2; + hook.selector = selector; + hook.a1 = a1; + hook.a2 = a2; + hook.a3 = a3; + hook.value = selector(store2.getSnapshot(), a1, a2, a3); } } - if (isFixed && !isOffsetParentAnElement && documentElement) { - setLeftRTLScrollbarOffset(); - } - const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll) : createCoords(0); - const x = rect.left + scroll.scrollLeft - offsets.x - htmlOffset.x; - const y = rect.top + scroll.scrollTop - offsets.y - htmlOffset.y; - return { - x, - y, - width: rect.width, - height: rect.height - }; + return hook.value; } -function isStaticPositioned(element) { - return getComputedStyle2(element).position === "static"; +function useStoreLegacy(store2, selector, a1, a2, a3) { + return (0, import_with_selector.useSyncExternalStoreWithSelector)(store2.subscribe, store2.getSnapshot, store2.getSnapshot, (state) => selector(state, a1, a2, a3)); } -function getTrueOffsetParent(element, polyfill) { - if (!isHTMLElement(element) || getComputedStyle2(element).position === "fixed") { - return null; - } - if (polyfill) { - return polyfill(element); - } - let rawOffsetParent = element.offsetParent; - if (getDocumentElement(element) === rawOffsetParent) { - rawOffsetParent = rawOffsetParent.ownerDocument.body; + +// node_modules/.store/@base-ui/utils@0.5.0--7My7Lubseca8je9LSEweA/node_modules/@base-ui/utils/store/Store.mjs +var Store = class { + /** + * Creates a store with the given initial state, constructing the class it is called on. + * Calling it on a generic base class (e.g. `ReactStore.create(...)`) constructs that + * class but degrades the inferred instance type to `Store`; use `new` there instead. + */ + static create(state) { + return new this(state); } - return rawOffsetParent; -} -function getOffsetParent(element, polyfill) { - const win = getWindow(element); - if (isTopLayer(element)) { - return win; + /** + * The current state of the store. + * This property is updated immediately when the state changes as a result of calling {@link setState}, {@link update}, or {@link set}. + * To subscribe to state changes, use the {@link useState} method. The value returned by {@link useState} is updated after the component renders (similarly to React's useState). + * The values can be used directly (to avoid subscribing to the store) in effects or event handlers. + * + * Do not modify properties in state directly. Instead, use the provided methods to ensure proper state management and listener notification. + */ + // Internal state to handle recursive `setState()` calls + constructor(state) { + this.state = state; + this.listeners = /* @__PURE__ */ new Set(); + this.updateTick = 0; } - if (!isHTMLElement(element)) { - let svgOffsetParent = getParentNode(element); - while (svgOffsetParent && !isLastTraversableNode(svgOffsetParent)) { - if (isElement(svgOffsetParent) && !isStaticPositioned(svgOffsetParent)) { - return svgOffsetParent; + /** + * Registers a listener that will be called whenever the store's state changes. + * + * @param fn The listener function to be called on state changes. + * @returns A function to unsubscribe the listener. + */ + subscribe = (fn) => { + this.listeners.add(fn); + return () => { + this.listeners.delete(fn); + }; + }; + /** + * Returns the current state of the store. + */ + getSnapshot = () => { + return this.state; + }; + /** + * Updates the entire store's state and notifies all registered listeners. + * + * @param newState The new state to set for the store. + */ + setState(newState) { + if (this.state === newState) { + return; + } + this.state = newState; + this.updateTick += 1; + const currentTick = this.updateTick; + for (const listener of this.listeners) { + if (currentTick !== this.updateTick) { + return; } - svgOffsetParent = getParentNode(svgOffsetParent); + listener(newState); } - return win; - } - let offsetParent = getTrueOffsetParent(element, polyfill); - while (offsetParent && isTableElement(offsetParent) && isStaticPositioned(offsetParent)) { - offsetParent = getTrueOffsetParent(offsetParent, polyfill); } - if (offsetParent && isLastTraversableNode(offsetParent) && isStaticPositioned(offsetParent) && !isContainingBlock(offsetParent)) { - return win; - } - return offsetParent || getContainingBlock(element) || win; -} -var getElementRects = async function(data) { - const getOffsetParentFn = this.getOffsetParent || getOffsetParent; - const getDimensionsFn = this.getDimensions; - const floatingDimensions = await getDimensionsFn(data.floating); - return { - reference: getRectRelativeToOffsetParent(data.reference, await getOffsetParentFn(data.floating), data.strategy), - floating: { - x: 0, - y: 0, - width: floatingDimensions.width, - height: floatingDimensions.height - } - }; -}; -function isRTL(element) { - return getComputedStyle2(element).direction === "rtl"; -} -var platform2 = { - convertOffsetParentRelativeRectToViewportRelativeRect, - getDocumentElement, - getClippingRect, - getOffsetParent, - getElementRects, - getClientRects, - getDimensions, - getScale, - isElement, - isRTL -}; -function rectsAreEqual(a, b) { - return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height; -} -function observeMove(element, onMove) { - let io = null; - let timeoutId; - const root = getDocumentElement(element); - function cleanup() { - var _io; - clearTimeout(timeoutId); - (_io = io) == null || _io.disconnect(); - io = null; - } - function refresh(skip, threshold) { - if (skip === void 0) { - skip = false; - } - if (threshold === void 0) { - threshold = 1; - } - cleanup(); - const elementRectForRootMargin = element.getBoundingClientRect(); - const { - left, - top, - width, - height - } = elementRectForRootMargin; - if (!skip) { - onMove(); - } - if (!width || !height) { - return; - } - const insetTop = floor(top); - const insetRight = floor(root.clientWidth - (left + width)); - const insetBottom = floor(root.clientHeight - (top + height)); - const insetLeft = floor(left); - const rootMargin = -insetTop + "px " + -insetRight + "px " + -insetBottom + "px " + -insetLeft + "px"; - const options = { - rootMargin, - threshold: max(0, min(1, threshold)) || 1 - }; - let isFirstUpdate = true; - function handleObserve(entries) { - const ratio = entries[0].intersectionRatio; - if (ratio !== threshold) { - if (!isFirstUpdate) { - return refresh(); - } - if (!ratio) { - timeoutId = setTimeout(() => { - refresh(false, 1e-7); - }, 1e3); - } else { - refresh(false, ratio); - } - } - if (ratio === 1 && !rectsAreEqual(elementRectForRootMargin, element.getBoundingClientRect())) { - refresh(); + /** + * Merges the provided changes into the current state and notifies listeners if there are changes. + * Each value must match its state key. Pass an exact known subset rather than a broad + * `Partial`, which may contain `undefined` for required state fields. + * + * @param changes An object containing the changes to apply to the current state. + */ + update(changes) { + for (const key in changes) { + if (!Object.is(this.state[key], changes[key])) { + this.setState({ + ...this.state, + ...changes + }); + return; } - isFirstUpdate = false; } - try { - io = new IntersectionObserver(handleObserve, { - ...options, - // Handle ', 'after_section' => '
', ), array( 'id' => 'test-section', 'title' => 'Section title', 'callback' => '__return_false', - 'before_section' => '
', 'after_section' => '
', 'section_class' => '', ), @@ -367,14 +367,14 @@ public function data_extra_args_for_add_settings_section() { 'disallowed tag in after_section' => array( array( 'before_section' => '
', - 'after_section' => '
', ), array( 'id' => 'test-section', 'title' => 'Section title', 'callback' => '__return_false', 'before_section' => '
', - 'after_section' => '
', 'section_class' => '', ), '
', @@ -442,21 +442,28 @@ public function test_settings_errors_css_classes( $type, $expected ) { $wp_settings_errors = null; - $expected = sprintf( 'notice %s settings-error is-dismissible', $expected ); - $this->assertStringContainsString( $expected, $output ); $this->assertStringNotContainsString( 'notice-notice-', $output ); } - public function data_settings_errors_css_classes() { + public static function data_settings_errors_css_classes() { return array( - array( 'error', 'notice-error' ), - array( 'success', 'notice-success' ), - array( 'warning', 'notice-warning' ), - array( 'info', 'notice-info' ), - array( 'updated', 'notice-success' ), - array( 'notice-error', 'notice-error' ), - array( 'error my-own-css-class hello world', 'error my-own-css-class hello world' ), + // The admin notices default types will generate CSS classes in this order. + array( 'error', 'notice notice-error is-dismissible settings-error' ), + array( 'success', 'notice notice-success is-dismissible settings-error' ), + array( 'warning', 'notice notice-warning is-dismissible settings-error' ), + array( 'info', 'notice notice-info is-dismissible settings-error' ), + array( 'updated', 'notice notice-success is-dismissible settings-error' ), + /* + * Backward compatibility: custom types and anything else (e.g. + * strings with spaces) are now passed as additional_classes. As + * such, the classes will be appended to the end of the generated + * class attribute. + */ + array( 'notice-error', 'notice is-dismissible settings-error notice-error' ), + array( 'custom', 'notice is-dismissible settings-error custom' ), + array( 'error my-own-css-class hello world', 'notice is-dismissible settings-error error my-own-css-class hello world' ), + array( 'error my-notice-is-awesome hello world', 'notice is-dismissible settings-error error my-notice-is-awesome hello world' ), ); } @@ -535,7 +542,7 @@ public function test_get_submit_button_expands_type_shorthands( $type, $expected * * @return array[] */ - public function data_get_submit_button_shorthand() { + public static function data_get_submit_button_shorthand() { return array( 'primary shorthand' => array( 'primary', 'button button-primary' ), 'small shorthand' => array( 'small', 'button button-small' ), diff --git a/tests/phpunit/tests/admin/includesTheme.php b/tests/phpunit/tests/admin/includesTheme.php index 0bb3044e6ba6e..24d037442852d 100644 --- a/tests/phpunit/tests/admin/includesTheme.php +++ b/tests/phpunit/tests/admin/includesTheme.php @@ -184,17 +184,59 @@ public function test_page_templates_child_theme() { } /** - * Test that the list of theme features pulled from the WordPress.org API returns the expected data structure. + * Test that a mocked WordPress.org API response returns the expected translated list of theme features. * - * Differences in the structure can also trigger failure by causing PHP notices/warnings. - * - * @group external-http * @ticket 28121 */ public function test_get_theme_featured_list_api() { - wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + + if ( is_multisite() ) { + // In multisite, only Super Admins can install themes, and the API is not queried without that capability. + grant_super_admin( $user_id ); + } + + wp_set_current_user( $user_id ); + + add_filter( + 'pre_http_request', + static function () { + return array( + 'headers' => array(), + 'body' => wp_json_encode( + array( + 'Subject' => array( 'blog', 'news' ), + 'Features' => array( 'custom-logo', 'unknown-feature' ), + 'Layout' => array( 'one-column' ), + ) + ), + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'cookies' => array(), + 'filename' => null, + ); + } + ); + $featured_list_api = get_theme_feature_list( true ); - $this->assertNonEmptyMultidimensionalArray( $featured_list_api ); + $this->assertSame( + array( + 'Subject' => array( + 'blog' => 'Blog', + 'news' => 'News', + ), + 'Features' => array( + 'custom-logo' => 'Custom Logo', + 'unknown-feature' => 'unknown-feature', + ), + 'Layout' => array( + 'one-column' => 'One Column', + ), + ), + $featured_list_api + ); } /** diff --git a/tests/phpunit/tests/admin/plugin-dependencies/base.php b/tests/phpunit/tests/admin/plugin-dependencies/base.php index 22c509807b205..bcec6f7da65d8 100644 --- a/tests/phpunit/tests/admin/plugin-dependencies/base.php +++ b/tests/phpunit/tests/admin/plugin-dependencies/base.php @@ -60,15 +60,29 @@ public static function tear_down_after_class() { parent::tear_down_after_class(); } + /** + * Resets all static properties to a default value before each test. + */ + public function set_up() { + parent::set_up(); + $this->reset_static_properties(); + } + /** * Resets all static properties to a default value after each test. */ public function tear_down() { + $this->reset_static_properties(); + parent::tear_down(); + } + + /** + * Resets all static properties to their default values. + */ + private function reset_static_properties() { foreach ( self::$static_properties as $name => $default_value ) { $this->set_property_value( $name, $default_value ); } - - parent::tear_down(); } /** @@ -78,9 +92,7 @@ public function tear_down() { * @param mixed $value The new value. */ public function set_property_value( $property, $value ) { - if ( ! isset( self::$reflected_members[ $property ] ) ) { - self::$reflected_members[ $property ] = new ReflectionProperty( self::$instance, $property ); - } + self::$reflected_members[ $property ] ??= new ReflectionProperty( self::$instance, $property ); if ( PHP_VERSION_ID < 80100 ) { self::$reflected_members[ $property ]->setAccessible( true ); @@ -98,9 +110,7 @@ public function set_property_value( $property, $value ) { * @return mixed The value of the property. */ public function get_property_value( $property ) { - if ( ! isset( self::$reflected_members[ $property ] ) ) { - self::$reflected_members[ $property ] = new ReflectionProperty( self::$instance, $property ); - } + self::$reflected_members[ $property ] ??= new ReflectionProperty( self::$instance, $property ); if ( PHP_VERSION_ID < 80100 ) { self::$reflected_members[ $property ]->setAccessible( true ); @@ -122,9 +132,7 @@ public function get_property_value( $property ) { * @return mixed The result of the method call. */ protected function call_method( $method, ...$args ) { - if ( ! isset( self::$reflected_members[ $method ] ) ) { - self::$reflected_members[ $method ] = new ReflectionMethod( self::$instance, $method ); - } + self::$reflected_members[ $method ] ??= new ReflectionMethod( self::$instance, $method ); if ( PHP_VERSION_ID < 80100 ) { self::$reflected_members[ $method ]->setAccessible( true ); diff --git a/tests/phpunit/tests/admin/plugin-dependencies/getCircularDependencies.php b/tests/phpunit/tests/admin/plugin-dependencies/getCircularDependencies.php new file mode 100644 index 0000000000000..2fc68e25a8ec9 --- /dev/null +++ b/tests/phpunit/tests/admin/plugin-dependencies/getCircularDependencies.php @@ -0,0 +1,133 @@ +set_dependencies( array( 'a' => array( 'a' ) ) ); + $this->set_property_value( 'circular_dependencies_pairs', array( array( 'x', 'y' ) ) ); + + $this->assertSame( array( array( 'x', 'y' ) ), $this->call_method( 'get_circular_dependencies' ), 'The stored pairs should be returned.' ); + $this->assertNull( $this->get_property_value( 'circular_dependencies_slugs' ), 'The slugs should not be computed.' ); + } + + /** + * Tests that an empty array is returned before the dependencies are read. + * + * @ticket 65819 + */ + public function test_should_return_empty_array_without_dependencies() { + $this->assertSame( array(), $this->call_method( 'get_circular_dependencies' ), 'No pairs should be returned.' ); + $this->assertNull( $this->get_property_value( 'circular_dependencies_pairs' ), 'The pairs should not be stored.' ); + } + + /** + * Tests the circular dependency pairs and slugs found. + * + * @ticket 65819 + * + * @dataProvider data_dependencies + * + * @param array $dependencies The dependency slugs of each plugin, keyed by plugin slug. + * @param array $pairs The expected pairs. + * @param array $slugs The expected slugs, in any order. + */ + public function test_should_find_circular_dependencies( $dependencies, $pairs, $slugs ) { + $this->set_dependencies( $dependencies ); + + $this->assertSame( $pairs, $this->call_method( 'get_circular_dependencies' ), 'The pairs should match.' ); + $this->assertSame( $pairs, $this->get_property_value( 'circular_dependencies_pairs' ), 'The pairs should be stored.' ); + $this->assertSameSets( $slugs, array_unique( $this->get_property_value( 'circular_dependencies_slugs' ) ), 'The slugs should match.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_dependencies() { + return array( + 'no circular dependency' => array( + array( + 'a' => array( 'b' ), + 'b' => array( 'c' ), + ), + array(), + array(), + ), + 'dependency that is not installed' => array( + array( 'a' => array( 'missing' ) ), + array(), + array(), + ), + 'plugin that depends on itself' => array( + array( 'a' => array( 'a' ) ), + array( array( 'a', 'a' ) ), + array( 'a' ), + ), + 'self-dependency next to another one' => array( + array( + 'a' => array( 'a', 'b' ), + 'b' => array(), + ), + array( array( 'a', 'a' ) ), + array( 'a' ), + ), + 'two plugins that depend on each other' => array( + array( + 'a' => array( 'b' ), + 'b' => array( 'a' ), + ), + array( array( 'a', 'b' ), array( 'b', 'a' ) ), + array( 'a', 'b' ), + ), + 'three plugins in a loop' => array( + array( + 'a' => array( 'b' ), + 'b' => array( 'c' ), + 'c' => array( 'a' ), + ), + array( array( 'a', 'c' ), array( 'b', 'a' ), array( 'c', 'b' ) ), + array( 'a', 'b', 'c' ), + ), + ); + } + + /** + * Sets the dependencies, keyed by plugin slug, as read from the plugin headers. + * + * @param array $dependencies The dependency slugs of each plugin, keyed by plugin slug. + */ + private function set_dependencies( $dependencies ) { + $by_file = array(); + $dependents = array(); + + foreach ( $dependencies as $slug => $slugs ) { + $file = "{$slug}/{$slug}.php"; + + $by_file[ $file ] = $slugs; + $dependents[ $file ] = $slug; + } + + $this->set_property_value( 'dependencies', $by_file ); + $this->set_property_value( 'dependent_slugs', $dependents ); + } +} diff --git a/tests/phpunit/tests/admin/plugin-dependencies/getDependencyFilepath.php b/tests/phpunit/tests/admin/plugin-dependencies/getDependencyFilepath.php index 04b4948e63f72..68b0fb2801ca1 100644 --- a/tests/phpunit/tests/admin/plugin-dependencies/getDependencyFilepath.php +++ b/tests/phpunit/tests/admin/plugin-dependencies/getDependencyFilepath.php @@ -69,7 +69,7 @@ public function test_should_return_filepaths_for_installed_dependencies( $depend * * @return array[] */ - public function data_get_dependency_filepath() { + public static function data_get_dependency_filepath() { return array( 'no plugins' => array( 'dependency_slug' => 'dependency', diff --git a/tests/phpunit/tests/admin/plugin-dependencies/getDependentFilepath.php b/tests/phpunit/tests/admin/plugin-dependencies/getDependentFilepath.php index 1cfdb30b137bf..fb82323847d64 100644 --- a/tests/phpunit/tests/admin/plugin-dependencies/getDependentFilepath.php +++ b/tests/phpunit/tests/admin/plugin-dependencies/getDependentFilepath.php @@ -43,7 +43,7 @@ public function test_should_return_filepaths_for_installed_dependents( $dependen * * @return array[] */ - public function data_get_dependent_filepath() { + public static function data_get_dependent_filepath() { return array( 'a plugin that exists' => array( 'dependent_slug' => 'dependent', diff --git a/tests/phpunit/tests/admin/plugin-dependencies/hasCircularDependency.php b/tests/phpunit/tests/admin/plugin-dependencies/hasCircularDependency.php index 10d00d96b7637..a43a4f5025853 100644 --- a/tests/phpunit/tests/admin/plugin-dependencies/hasCircularDependency.php +++ b/tests/phpunit/tests/admin/plugin-dependencies/hasCircularDependency.php @@ -77,7 +77,7 @@ public function test_should_return_true_when_a_plugin_has_circular_dependency( $ * * @return array[] */ - public function data_circular_dependencies() { + public static function data_circular_dependencies() { return array( 'a plugin that depends on itself' => array( 'plugin_to_check' => 'dependency/dependency.php', diff --git a/tests/phpunit/tests/admin/plugin-dependencies/initialize.php b/tests/phpunit/tests/admin/plugin-dependencies/initialize.php index 375e30c4bd6e2..58e3392dcd8b0 100644 --- a/tests/phpunit/tests/admin/plugin-dependencies/initialize.php +++ b/tests/phpunit/tests/admin/plugin-dependencies/initialize.php @@ -62,7 +62,7 @@ public function test_should_only_initialize_once( $property_name ) { * * @return array[] */ - public function data_static_properties_set_during_initialization() { + public static function data_static_properties_set_during_initialization() { /* * This does not include 'dependency_api_data' as it is only set * on certain pages. This is tested later. @@ -117,7 +117,7 @@ public function test_should_set_dependency_api_data_on_certain_screens( $screen * * @return array[] */ - public function data_screens() { + public static function data_screens() { return array( 'plugins.php' => array( 'screen' => 'plugins.php', @@ -167,7 +167,7 @@ public function test_initialize_should_load_and_sanitize_dependency_slugs_from_p * * @return array[] */ - public function data_should_sanitize_slugs() { + public static function data_should_sanitize_slugs() { return array( // Valid slugs. 'one dependency' => array( diff --git a/tests/phpunit/tests/admin/pluginInstallerSkinDoOverwrite.php b/tests/phpunit/tests/admin/pluginInstallerSkinDoOverwrite.php new file mode 100644 index 0000000000000..dac1aa65bf44a --- /dev/null +++ b/tests/phpunit/tests/admin/pluginInstallerSkinDoOverwrite.php @@ -0,0 +1,454 @@ +remove_plugin(); + } + + /** + * Removes the test plugin and clears the plugins cache. + */ + public function tear_down() { + $this->remove_plugin(); + + parent::tear_down(); + } + + /** + * Tests that nothing is output unless an upload hit an existing plugin folder. + * + * @ticket 65819 + * + * @dataProvider data_not_an_overwrite + * + * @param string $type The skin type. + * @param string $error_code The result error code, or an empty string for a successful result. + */ + public function test_should_return_false_when_not_overwriting( $type, $error_code ) { + $this->create_plugin( array( 'Version' => '1.0.0' ) ); + + $result = '' === $error_code ? true : new WP_Error( $error_code, 'Error.', WP_PLUGIN_DIR . '/' . self::SLUG . '/' ); + $skin = $this->get_skin( $result, array( 'Name' => 'Overwrite Test Plugin' ), $type ); + + $this->assertSame( array( false, '' ), $this->do_overwrite( $skin ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_not_an_overwrite() { + return array( + 'web install' => array( 'web', 'folder_exists' ), + 'successful upload' => array( 'upload', '' ), + 'other upload error' => array( 'upload', 'incompatible_archive' ), + ); + } + + /** + * Tests that nothing is output when the existing folder holds no plugin. + * + * @ticket 65819 + */ + public function test_should_return_false_when_existing_folder_has_no_plugin() { + $this->create_plugin( array( 'Version' => '1.0.0' ) ); + + $skin = $this->get_skin( + new WP_Error( 'folder_exists', 'Error.', WP_PLUGIN_DIR . '/other-plugin/' ), + array( 'Name' => 'Overwrite Test Plugin' ) + ); + + $this->assertSame( array( false, '' ), $this->do_overwrite( $skin ) ); + } + + /** + * Tests that nothing is output when the uploaded plugin data is missing. + * + * @ticket 65819 + */ + public function test_should_return_false_without_uploaded_plugin_data() { + $this->create_plugin( array( 'Version' => '1.0.0' ) ); + + $skin = $this->get_skin( $this->get_folder_exists_error(), array() ); + + $this->assertSame( array( false, '' ), $this->do_overwrite( $skin ) ); + } + + /** + * Tests the comparison table, notice and actions when updating a plugin. + * + * @ticket 65819 + */ + public function test_should_output_update_comparison_and_actions() { + $this->create_plugin( + array( + 'Version' => '1.0.0', + 'Author' => 'Jane Doe', + 'RequiresPHP' => '7.4', + ) + ); + + $skin = $this->get_skin( + $this->get_folder_exists_error(), + array( + 'Name' => 'Overwrite Test Plugin', + 'Version' => '2.0.0', + 'Author' => 'Jane & Co', + 'RequiresWP' => '5.0', + 'RequiresPHP' => '', + ) + ); + + $this->assertSame( + array( + true, + '

This plugin is already installed.

' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
CurrentUploaded
Plugin nameOverwrite Test PluginOverwrite Test Plugin
Version1.0.02.0.0
AuthorJane DoeJane & Co
Required WordPress version-5.0
Required PHP version7.4-
' + . '

You are updating a plugin. Be sure to back up your database and files first.

' + . $this->get_expected_actions( 'update-plugin' ), + ), + $this->do_overwrite( $skin ) + ); + } + + /** + * Tests the version warning, notice and overwrite URL when downgrading a plugin. + * + * @ticket 65819 + */ + public function test_should_output_downgrade_warning() { + $this->create_plugin( array( 'Version' => '2.0.0' ) ); + + $skin = $this->get_skin( + $this->get_folder_exists_error(), + array( + 'Name' => 'Overwrite Test Plugin', + 'Version' => '1.9.9', + ) + ); + + list( $overwrites, $output ) = $this->do_overwrite( $skin ); + + $this->assertTrue( $overwrites, 'The overwrite screen should be shown.' ); + $this->assertStringContainsString( + 'Version2.0.01.9.9', + $output, + 'The uploaded version should be marked as a warning.' + ); + $this->assertStringContainsString( + '

You are uploading an older version of a current plugin. You can continue to install the older version, but be sure to back up your database and files first.

', + $output, + 'The downgrade notice should be shown.' + ); + $this->assertStringEndsWith( $this->get_expected_actions( 'downgrade-plugin' ), $output, 'The overwrite URL should downgrade the plugin.' ); + } + + /** + * Tests that the plugin cannot be replaced when the uploaded plugin's requirements are not met. + * + * @ticket 65819 + * + * @dataProvider data_unmet_requirements + * + * @param array $requirements The uploaded plugin's requirements. + * @param string[] $reasons The expected reasons, as format strings for PHP_VERSION and the WordPress version. + */ + public function test_should_block_update_when_requirements_are_not_met( $requirements, $reasons ) { + $this->create_plugin( array( 'Version' => '1.0.0' ) ); + + $skin = $this->get_skin( + $this->get_folder_exists_error(), + array_merge( + array( + 'Name' => 'Overwrite Test Plugin', + 'Version' => '2.0.0', + ), + $requirements + ) + ); + + list( $overwrites, $output ) = $this->do_overwrite( $skin ); + + $items = ''; + foreach ( $reasons as $reason ) { + $items .= '
  • ' . sprintf( $reason, PHP_VERSION, wp_get_wp_version() ) . '
  • '; + } + + $this->assertTrue( $overwrites, 'The overwrite screen should be shown.' ); + $this->assertStringContainsString( + '

    The plugin cannot be updated due to the following:

      ' . $items . '
    ', + $output, + 'The reasons should be listed after the table.' + ); + $this->assertStringNotContainsString( 'update-from-upload-notice', $output, 'No backup notice should be shown.' ); + $this->assertStringEndsWith( $this->get_expected_actions( '' ), $output, 'Only the cancel action should be shown.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_unmet_requirements() { + $php = 'The PHP version on your server is %1$s, however the uploaded plugin requires 99.0.'; + $wp = 'Your WordPress version is %2$s, however the uploaded plugin requires 99.0.'; + + return array( + 'PHP' => array( array( 'RequiresPHP' => '99.0' ), array( $php ) ), + 'WordPress' => array( array( 'RequiresWP' => '99.0' ), array( $wp ) ), + 'PHP and WordPress' => array( + array( + 'RequiresPHP' => '99.0', + 'RequiresWP' => '99.0', + ), + array( $php, $wp ), + ), + ); + } + + /** + * Tests that the comparison table can be filtered. + * + * @ticket 65819 + */ + public function test_should_filter_comparison_table() { + $this->create_plugin( array( 'Version' => '1.0.0' ) ); + + $new_plugin_data = array( + 'Name' => 'Overwrite Test Plugin', + 'Version' => '2.0.0', + ); + + $filter = new MockAction(); + add_filter( 'install_plugin_overwrite_comparison', array( $filter, 'filter' ), 10, 3 ); + add_filter( + 'install_plugin_overwrite_comparison', + static function () { + return '
    '; + }, + 11 + ); + + list( , $output ) = $this->do_overwrite( $this->get_skin( $this->get_folder_exists_error(), $new_plugin_data ) ); + + $args = $filter->get_args(); + + $this->assertStringStartsWith( '', $args[0][0], 'The filter should receive the table.' ); + $this->assertSame( 'Overwrite Test Plugin', $args[0][1]['Name'], 'The filter should receive the current plugin data.' ); + $this->assertSame( $new_plugin_data, $args[0][2], 'The filter should receive the uploaded plugin data.' ); + $this->assertStringContainsString( '

    ', $output, 'The filtered table should be output.' ); + } + + /** + * Tests that the actions can be filtered, and none are output when they are removed. + * + * @ticket 65819 + */ + public function test_should_filter_actions() { + $this->create_plugin( array( 'Version' => '1.0.0' ) ); + + $new_plugin_data = array( + 'Name' => 'Overwrite Test Plugin', + 'Version' => '2.0.0', + ); + + $filter = new MockAction(); + add_filter( 'install_plugin_overwrite_actions', array( $filter, 'filter' ), 10, 3 ); + add_filter( 'install_plugin_overwrite_actions', '__return_empty_array', 11 ); + + $skin = $this->get_skin( $this->get_folder_exists_error(), $new_plugin_data ); + + list( $overwrites, $output ) = $this->do_overwrite( $skin ); + + $args = $filter->get_args(); + + $this->assertTrue( $overwrites, 'The overwrite screen should be shown.' ); + $this->assertSame( array( 'overwrite_plugin', 'plugins_page' ), array_keys( $args[0][0] ), 'The filter should receive both actions.' ); + $this->assertSame( $skin->api, $args[0][1], 'The filter should receive the API data.' ); + $this->assertSame( $new_plugin_data, $args[0][2], 'The filter should receive the uploaded plugin data.' ); + $this->assertStringEndsWith( 'first.

    ', $output, 'No actions should be output.' ); + } + + /** + * Tests that after() stops after the overwrite screen. + * + * @ticket 65819 + * + * @covers Plugin_Installer_Skin::after + */ + public function test_after_should_only_output_overwrite_screen() { + $this->create_plugin( array( 'Version' => '1.0.0' ) ); + + $skin = $this->get_skin( + $this->get_folder_exists_error(), + array( + 'Name' => 'Overwrite Test Plugin', + 'Version' => '2.0.0', + ) + ); + + $output = get_echo( array( $skin, 'after' ) ); + + $this->assertStringStartsWith( '

    ', $output, 'The overwrite screen should be output.' ); + $this->assertStringNotContainsString( 'Activate Plugin', $output, 'The install actions should not be output.' ); + } + + /** + * Returns the expected actions markup. + * + * @param string $overwrite The overwrite query argument, or an empty string when the plugin cannot be replaced. + * @return string The markup. + */ + private function get_expected_actions( $overwrite ) { + $actions = array(); + + if ( '' !== $overwrite ) { + $actions[] = 'Replace current with uploaded'; + } + + $actions[] = 'Cancel and go back'; + + return '' + . '

    ' . implode( ' ', $actions ) . '

    '; + } + + /** + * Returns the error the upgrader returns when the plugin folder already exists. + * + * @return WP_Error The error. + */ + private function get_folder_exists_error() { + return new WP_Error( 'folder_exists', 'Destination folder already exists.', WP_PLUGIN_DIR . '/' . self::SLUG . '/' ); + } + + /** + * Returns a skin with the given result and uploaded plugin data. + * + * @param true|WP_Error $result The install result. + * @param array $new_plugin_data The uploaded plugin data. + * @param string $type The skin type. + * @return Plugin_Installer_Skin The skin. + */ + private function get_skin( $result, $new_plugin_data, $type = 'upload' ) { + $skin = new Plugin_Installer_Skin( + array( + 'type' => $type, + 'url' => self::URL, + ) + ); + + $upgrader = new Plugin_Upgrader( $skin ); + $upgrader->new_plugin_data = $new_plugin_data; + + $skin->upgrader = $upgrader; + $skin->result = $result; + + return $skin; + } + + /** + * Calls the private do_overwrite() method. + * + * @param Plugin_Installer_Skin $skin The skin. + * @return array The return value and the output. + */ + private function do_overwrite( $skin ) { + $method = new ReflectionMethod( $skin, 'do_overwrite' ); + if ( PHP_VERSION_ID < 80100 ) { + $method->setAccessible( true ); + } + + ob_start(); + $result = $method->invoke( $skin ); + $output = ob_get_clean(); + + return array( $result, $output ); + } + + /** + * Creates the installed test plugin. + * + * @param array $headers The Version, Author, RequiresWP and RequiresPHP headers. + */ + private function create_plugin( $headers ) { + $names = array( + 'Version' => 'Version', + 'Author' => 'Author', + 'RequiresWP' => 'Requires at least', + 'RequiresPHP' => 'Requires PHP', + ); + + $contents = " $value ) { + $contents .= " * {$names[ $field ]}: {$value}\n"; + } + $contents .= " */\n"; + + $dir = WP_PLUGIN_DIR . '/' . self::SLUG; + + $this->assertTrue( mkdir( $dir ), 'The test plugin directory should be created.' ); + $this->assertNotFalse( file_put_contents( $dir . '/' . self::SLUG . '.php', $contents ), 'The test plugin file should be written.' ); + + wp_cache_delete( 'plugins', 'plugins' ); + } + + /** + * Removes the test plugin and clears the plugins cache. + */ + private function remove_plugin() { + $dir = WP_PLUGIN_DIR . '/' . self::SLUG; + + if ( is_dir( $dir ) ) { + $this->rmdir( $dir ); + rmdir( $dir ); + } + + wp_cache_delete( 'plugins', 'plugins' ); + } +} diff --git a/tests/phpunit/tests/admin/pluginUpgraderCheckPackage.php b/tests/phpunit/tests/admin/pluginUpgraderCheckPackage.php new file mode 100644 index 0000000000000..d33f436d6d340 --- /dev/null +++ b/tests/phpunit/tests/admin/pluginUpgraderCheckPackage.php @@ -0,0 +1,264 @@ +remove_package(); + + $this->had_filesystem = array_key_exists( 'wp_filesystem', $GLOBALS ); + $this->original_filesystem = $this->had_filesystem ? $GLOBALS['wp_filesystem'] : null; + + $this->set_filesystem_content_dir( trailingslashit( WP_CONTENT_DIR ) ); + + $this->upgrader = new Plugin_Upgrader( $this->createMock( 'WP_Upgrader_Skin' ) ); + $this->upgrader->generic_strings(); + } + + /** + * Restores the filesystem global and removes the package directory. + */ + public function tear_down() { + if ( $this->had_filesystem ) { + $GLOBALS['wp_filesystem'] = $this->original_filesystem; + } else { + unset( $GLOBALS['wp_filesystem'] ); + } + + $this->remove_package(); + + parent::tear_down(); + } + + /** + * Tests that an error source is returned unchanged and the uploaded plugin data is reset. + * + * @ticket 65819 + */ + public function test_should_return_error_source_and_reset_plugin_data() { + $this->upgrader->new_plugin_data = array( 'Name' => 'Previous Plugin' ); + + $source = new WP_Error( 'download_failed', 'Download failed.' ); + + $this->assertSame( $source, $this->upgrader->check_package( $source ), 'The error should be returned.' ); + $this->assertSame( array(), $this->upgrader->new_plugin_data, 'The uploaded plugin data should be reset.' ); + } + + /** + * Tests that a source that is not a local directory is returned unchanged. + * + * @ticket 65819 + */ + public function test_should_return_source_when_directory_does_not_exist() { + $source = trailingslashit( WP_CONTENT_DIR ) . self::PACKAGE . '/'; + + $this->assertSame( $source, $this->upgrader->check_package( $source ) ); + } + + /** + * Tests that a package without a valid plugin is rejected. + * + * @ticket 65819 + * + * @dataProvider data_packages_without_plugin + * + * @param array $files The package files, keyed by file name. + */ + public function test_should_reject_package_without_plugin( $files ) { + $source = $this->create_package( $files ); + + $result = $this->upgrader->check_package( $source ); + + $this->assertWPError( $result, 'The package should be rejected.' ); + $this->assertSame( 'incompatible_archive_no_plugins', $result->get_error_code(), 'The error code should match.' ); + $this->assertSame( 'The package could not be installed.', $result->get_error_message(), 'The error message should match.' ); + $this->assertSame( 'No valid plugins were found.', $result->get_error_data(), 'The error data should explain why.' ); + $this->assertSame( array(), $this->upgrader->new_plugin_data, 'No plugin data should be stored.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_packages_without_plugin() { + return array( + 'empty package' => array( array() ), + 'no PHP files' => array( array( 'readme.txt' => "Plugin Name: Not A PHP File\n" ) ), + 'PHP file without plugin name' => array( array( 'functions.php' => " array( array( 'includes/plugin.php' => "create_package( + array( + 'a-functions.php' => " " "assertSame( $source, $this->upgrader->check_package( $source ), 'The source should be returned.' ); + $this->assertSame( 'Check Package Plugin', $this->upgrader->new_plugin_data['Name'], 'The first plugin should be stored.' ); + $this->assertSame( '1.2.3', $this->upgrader->new_plugin_data['Version'], 'The plugin version should be stored.' ); + } + + /** + * Tests that a remote filesystem path is mapped to the local content directory. + * + * @ticket 65819 + */ + public function test_should_map_remote_path_to_local_content_directory() { + $this->create_package( array( 'plugin.php' => "set_filesystem_content_dir( '/remote/wp-content/' ); + + $source = '/remote/wp-content/' . self::PACKAGE . '/'; + + $this->assertSame( $source, $this->upgrader->check_package( $source ), 'The remote source should be returned.' ); + $this->assertSame( 'Remote Plugin', $this->upgrader->new_plugin_data['Name'], 'The plugin should be read from the local directory.' ); + } + + /** + * Tests that a plugin whose requirements are not met is rejected. + * + * @ticket 65819 + * + * @dataProvider data_unmet_requirements + * + * @param string $headers The requirement headers. + * @param string $code The expected error code. + * @param string $reason The expected reason, as a format string for PHP_VERSION and the WordPress version. + */ + public function test_should_reject_plugin_with_unmet_requirements( $headers, $code, $reason ) { + $source = $this->create_package( array( 'plugin.php' => "upgrader->check_package( $source ); + + $this->assertWPError( $result, 'The package should be rejected.' ); + $this->assertSame( $code, $result->get_error_code(), 'The error code should match.' ); + $this->assertSame( 'The package could not be installed.', $result->get_error_message(), 'The error message should match.' ); + $this->assertSame( sprintf( $reason, PHP_VERSION, wp_get_wp_version() ), $result->get_error_data(), 'The error data should explain why.' ); + $this->assertSame( 'Demanding Plugin', $this->upgrader->new_plugin_data['Name'], 'The plugin data should still be stored.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_unmet_requirements() { + $php = 'The PHP version on your server is %1$s, however the uploaded plugin requires 99.0.'; + $wp = 'Your WordPress version is %2$s, however the uploaded plugin requires 99.0.'; + + return array( + 'PHP' => array( " * Requires PHP: 99.0\n", 'incompatible_php_required_version', $php ), + 'WordPress' => array( " * Requires at least: 99.0\n", 'incompatible_wp_required_version', $wp ), + 'PHP and WordPress, PHP is reported' => array( " * Requires PHP: 99.0\n * Requires at least: 99.0\n", 'incompatible_php_required_version', $php ), + ); + } + + /** + * Creates the package directory with the given files. + * + * @param array $files The file contents, keyed by file name relative to the package. + * @return string The package directory, with a trailing slash. + */ + private function create_package( $files ) { + $dir = trailingslashit( WP_CONTENT_DIR ) . self::PACKAGE . '/'; + + $this->assertTrue( mkdir( $dir ), 'The package directory should be created.' ); + + foreach ( $files as $name => $contents ) { + $file = $dir . $name; + + if ( ! is_dir( dirname( $file ) ) ) { + $this->assertTrue( mkdir( dirname( $file ), 0777, true ), 'The package subdirectory should be created.' ); + } + + $this->assertNotFalse( file_put_contents( $file, $contents ), 'The package file should be written.' ); + } + + return $dir; + } + + /** + * Removes the package directory. + */ + private function remove_package() { + $dir = trailingslashit( WP_CONTENT_DIR ) . self::PACKAGE; + + if ( is_dir( $dir ) ) { + $this->rmdir( $dir ); + $this->delete_folders( $dir ); + } + } + + /** + * Sets a filesystem whose content directory is the given path. + * + * @param string $content_dir The content directory path of the filesystem. + */ + private function set_filesystem_content_dir( $content_dir ) { + $filesystem = $this->createMock( 'WP_Filesystem_Base' ); + $filesystem->method( 'wp_content_dir' )->willReturn( $content_dir ); + + $GLOBALS['wp_filesystem'] = $filesystem; + } +} diff --git a/tests/phpunit/tests/admin/walkerCategoryChecklist.php b/tests/phpunit/tests/admin/walkerCategoryChecklist.php new file mode 100644 index 0000000000000..5cf2f2a5498b0 --- /dev/null +++ b/tests/phpunit/tests/admin/walkerCategoryChecklist.php @@ -0,0 +1,316 @@ + 5, + 'name' => 'News', + 'parent' => 0, + ), + $fields + ); + } + + /** + * Generates the list item of a category and returns a tag processor positioned at its LI element. + * + * @param array $args Arguments passed to the walker. + * @return WP_HTML_Tag_Processor Tag processor. + */ + private function get_checkbox_item_processor( array $args ) { + $walker = new Walker_Category_Checklist(); + $output = ''; + + $walker->start_el( $output, $this->get_category(), 0, $args ); + + $processor = new WP_HTML_Tag_Processor( $output ); + + $this->assertTrue( $processor->next_tag( 'LI' ), 'The output should start with a list item.' ); + + return $processor; + } + + /** + * Tests the markup that opens and closes a level and closes a list item. + * + * @ticket 65819 + * + * @covers ::start_lvl + * @covers ::end_lvl + * @covers ::end_el + * + * @dataProvider data_depths + * + * @param int $depth Depth of the level. + * @param string $indent Expected indentation. + */ + public function test_level_and_element_wrappers( $depth, $indent ) { + $walker = new Walker_Category_Checklist(); + + $start_lvl = 'before'; + $walker->start_lvl( $start_lvl, $depth ); + + $end_lvl = 'before'; + $walker->end_lvl( $end_lvl, $depth ); + + $end_el = 'before'; + $walker->end_el( $end_el, $this->get_category(), $depth ); + + $this->assertSame( "before{$indent}
      \n", $start_lvl, 'A level should open with an indented list.' ); + $this->assertSame( "before{$indent}
    \n", $end_lvl, 'A level should close with an indented list end tag.' ); + $this->assertSame( "before\n", $end_el, 'An element should close with a list item end tag.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_depths() { + return array( + 'top level' => array( 0, '' ), + 'first level' => array( 1, "\t" ), + 'second level' => array( 2, "\t\t" ), + ); + } + + /** + * Tests the list item generated in list-only mode. + * + * @ticket 65819 + * + * @covers ::start_el + * + * @dataProvider data_list_only_items + * + * @param array $args Arguments passed to the walker. + * @param string $expected Expected list item, without the leading newline. + */ + public function test_start_el_in_list_only_mode( $args, $expected ) { + $args['list_only'] = true; + + $walker = new Walker_Category_Checklist(); + $output = ''; + + $walker->start_el( $output, $this->get_category(), 0, $args ); + + $this->assertSame( "\n" . $expected, $output ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_list_only_items() { + $unselected = ''; + $selected = ''; + + return array( + 'no selected or popular categories' => array( array(), '
  • ' . $unselected ), + 'empty selected categories' => array( array( 'selected_cats' => array() ), '
  • ' . $unselected ), + 'selected category' => array( array( 'selected_cats' => array( 4, 5 ) ), '
  • ' . $selected ), + 'selected category as a string' => array( array( 'selected_cats' => array( '5' ) ), '
  • ' . $selected ), + 'another selected category' => array( array( 'selected_cats' => array( 50 ) ), '
  • ' . $unselected ), + 'popular category' => array( array( 'popular_cats' => array( 5 ) ), '
  • ' . $unselected ), + 'popular and selected category' => array( + array( + 'popular_cats' => array( 5 ), + 'selected_cats' => array( 5 ), + ), + '
  • ' . + "
      \n" . + "\n" . '
    • ' . "
    • \n" . + "
    \n" . + "
  • \n" . + "\n" . '
  • ' . "
  • \n", + $output + ); + } +} diff --git a/tests/phpunit/tests/admin/wpApplicationPasswordsListTable.php b/tests/phpunit/tests/admin/wpApplicationPasswordsListTable.php new file mode 100644 index 0000000000000..2340c4307881c --- /dev/null +++ b/tests/phpunit/tests/admin/wpApplicationPasswordsListTable.php @@ -0,0 +1,55 @@ +table = _get_list_table( + 'WP_Application_Passwords_List_Table', + array( 'screen' => 'application-passwords-user' ) + ); + } + + /** + * Ensures the JavaScript row template used for a password created via AJAX + * renders the primary column as a `` row header, matching + * the server-rendered markup from WP_List_Table::single_row_columns(). + * + * @ticket 65707 + * + * @covers WP_Application_Passwords_List_Table::print_js_template_row + */ + public function test_print_js_template_row_uses_th_row_header_for_primary_column() { + ob_start(); + $this->table->print_js_template_row(); + $html = ob_get_clean(); + + // Assert on the tag structure and `scope="row"` rather than the localized + // column label, so the test does not depend on the current locale or on + // filters that adjust the column headers. + $this->assertMatchesRegularExpression( + '//', + $html, + 'The primary column should be rendered as a row header.' + ); + + $this->assertStringContainsString( + ' cells.' + ); + } +} diff --git a/tests/phpunit/tests/admin/wpAutomaticUpdater.php b/tests/phpunit/tests/admin/wpAutomaticUpdater.php index ee52f44daf9c1..eb7d982f2a0f6 100644 --- a/tests/phpunit/tests/admin/wpAutomaticUpdater.php +++ b/tests/phpunit/tests/admin/wpAutomaticUpdater.php @@ -40,6 +40,31 @@ public function set_up() { add_filter( 'pre_wp_mail', '__return_false' ); } + /** + * Builds an open_basedir value that allows PHPUnit to load Composer dependencies. + * + * Composer dependencies may live outside a secondary worktree, and PHPUnit can + * load assertion-related classes after the restriction is set. + * + * @param string $abspath_grandparent Directory containing the repository. + * @return string The open_basedir value. + */ + private function get_open_basedir_for_tests( $abspath_grandparent ) { + $composer_vendor_dir = dirname( + ( new ReflectionClass( Composer\Autoload\ClassLoader::class ) )->getFileName(), + 2 + ); + + return implode( + PATH_SEPARATOR, + array( + sys_get_temp_dir(), + wp_normalize_path( $abspath_grandparent ), + wp_normalize_path( $composer_vendor_dir ), + ) + ); + } + /** * Tests that `WP_Automatic_Updater::send_plugin_theme_email()` appends * plugin URLs. @@ -87,7 +112,7 @@ function ( $args ) use ( $urls ) { * * @return array */ - public function data_send_plugin_theme_email_should_append_plugin_urls() { + public static function data_send_plugin_theme_email_should_append_plugin_urls() { return array( 'successful updates, the current version and the plugin url' => array( 'urls' => array( 'http://example.org/successful-plugin' ), @@ -355,7 +380,7 @@ function ( $args ) use ( $urls ) { * * @return array */ - public function data_send_plugin_theme_email_should_not_append_plugin_urls() { + public static function data_send_plugin_theme_email_should_not_append_plugin_urls() { return array( 'successful updates, the current version, but no plugin url' => array( 'urls' => array( 'http://example.org/successful-plugin' ), @@ -612,8 +637,7 @@ public function test_is_allowed_dir_should_return_true_if_open_basedir_is_set_an $abspath_grandparent = trailingslashit( dirname( $abspath_parent ) ); $open_basedir_backup = ini_get( 'open_basedir' ); - // Allow access to the directory one level above the repository. - ini_set( 'open_basedir', sys_get_temp_dir() . PATH_SEPARATOR . wp_normalize_path( $abspath_grandparent ) ); + ini_set( 'open_basedir', $this->get_open_basedir_for_tests( $abspath_grandparent ) ); // Checking an allowed directory should succeed. $actual = self::$updater->is_allowed_dir( wp_normalize_path( ABSPATH ) ); @@ -647,8 +671,7 @@ public function test_is_allowed_dir_should_return_false_if_open_basedir_is_set_a $abspath_grandparent = trailingslashit( dirname( $abspath_parent ) ); $open_basedir_backup = ini_get( 'open_basedir' ); - // Allow access to the directory one level above the repository. - ini_set( 'open_basedir', sys_get_temp_dir() . PATH_SEPARATOR . wp_normalize_path( $abspath_grandparent ) ); + ini_set( 'open_basedir', $this->get_open_basedir_for_tests( $abspath_grandparent ) ); // Checking a directory not within the allowed path should trigger an `open_basedir` warning. $actual = self::$updater->is_allowed_dir( '/.git' ); @@ -681,7 +704,7 @@ public function test_is_allowed_dir_should_throw_doing_it_wrong_with_invalid_dir * * @return array[] */ - public function data_is_allowed_dir_should_throw_doing_it_wrong_with_invalid_dir() { + public static function data_is_allowed_dir_should_throw_doing_it_wrong_with_invalid_dir() { return array( // Type checks and boolean comparisons. 'null' => array( 'dir' => null ), @@ -733,4 +756,156 @@ public function test_is_vcs_checkout_should_return_false_when_no_directories_are $this->assertFalse( $updater_mock->is_vcs_checkout( get_temp_dir() ) ); } + + /** + * Tests that {@see WP_Automatic_Updater::send_plugin_theme_email()} lists the details of a + * fatal error under the plugin whose update was rolled back. + * + * @ticket 64155 + * + * @covers WP_Automatic_Updater::send_plugin_theme_email + */ + public function test_send_plugin_theme_email_should_include_fatal_error_details(): void { + $filter = new MockAction(); + add_filter( 'wp_mail', array( $filter, 'filter' ) ); + + $fatal_error = 'PHP Fatal error: Uncaught Error: Call to undefined function failed_plugin_init() in wp-content/plugins/failed-plugin/failed-plugin.php on line 12'; + + $result = new WP_Error( 'plugin_update_fatal_error_rollback_successful', "The update for 'failed-plugin' contained a fatal error. The previously installed version has been restored." ); + $result->add( 'plugin_update_fatal_error', $fatal_error ); + + $failed = array( + (object) array( + 'name' => 'Failed Plugin', + 'result' => $result, + 'item' => (object) array( + 'current_version' => '1.0.0', + 'new_version' => '2.0.0', + 'plugin' => 'failed-plugin/failed-plugin.php', + ), + ), + ); + + self::$send_plugin_theme_email->invokeArgs( self::$updater, array( 'fail', array(), array( 'plugin' => $failed ) ) ); + + $this->assertSame( 1, $filter->get_call_count(), 'An email should have been sent.' ); + + $mail_args = $filter->get_args()[0][0]; + $this->assertIsArray( $mail_args ); + $this->assertArrayHasKey( 'message', $mail_args ); + $this->assertIsString( $mail_args['message'] ); + $this->assertStringContainsString( + "- Failed Plugin (from version 1.0.0 to 2.0.0)\n {$fatal_error}\n", + $mail_args['message'], + 'The fatal error should be listed under the plugin that failed to update.' + ); + } + + /** + * Tests that {@see WP_Automatic_Updater::has_fatal_error()} records the details of the fatal + * error found by the loopback request, without any stack trace. + * + * @ticket 64155 + * + * @covers WP_Automatic_Updater::has_fatal_error + * + * @dataProvider data_has_fatal_error_should_record_fatal_error_details + * + * @param array|true|WP_Error $scrape_result The result of the loopback request: the error + * found by the scrape, `true` for none, or a + * `WP_Error` for a failed request. + * @param bool $expected_return The expected return value. + * @param string|null $expected_message The expected details of the fatal error. + */ + public function test_has_fatal_error_should_record_fatal_error_details( $scrape_result, bool $expected_return, ?string $expected_message ): void { + $upgrading = 1700000000; + + $maintenance_file = ABSPATH . '.maintenance'; + $this->assertFileDoesNotExist( $maintenance_file, 'The test requires that no maintenance file exists.' ); + file_put_contents( $maintenance_file, " array(), + 'body' => "\n###### wp_scraping_result_start:{$scrape_key} ######\n" . wp_json_encode( $scrape_result ) . "\n###### wp_scraping_result_end:{$scrape_key} ######\n", + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'cookies' => array(), + 'filename' => null, + ); + } + ); + + $has_fatal_error = new ReflectionMethod( self::$updater, 'has_fatal_error' ); + $message = new ReflectionProperty( self::$updater, 'fatal_error_message' ); + if ( PHP_VERSION_ID < 80100 ) { + $has_fatal_error->setAccessible( true ); + $message->setAccessible( true ); + } + + try { + $this->assertSame( $expected_return, $has_fatal_error->invoke( self::$updater ), 'The return value should indicate whether a fatal error was detected.' ); + } finally { + unlink( $maintenance_file ); + } + + $this->assertSame( $expected_message, $message->getValue( self::$updater ), 'The recorded details of the fatal error do not match.' ); + } + + /** + * Data provider. + * + * The scrape results mirror those of {@see wp_finalize_scraping_edited_file_errors()}, which + * passes the error through `str_replace()`, so that every value is a string. + * + * @return array|true|WP_Error, + * expected_return: bool, + * expected_message: string|null, + * }> + */ + public function data_has_fatal_error_should_record_fatal_error_details(): array { + return array( + 'fatal error with a stack trace' => array( + 'scrape_result' => array( + 'type' => '1', + 'message' => "Uncaught Exception: Could not connect to the database in wp-content/plugins/failed-plugin/failed-plugin.php:3\nStack trace:\n#0 wp-content/plugins/failed-plugin/failed-plugin.php(7): failed_plugin_connect('db_user', 'secret_password')\n#1 {main}\n thrown", + 'file' => 'wp-content/plugins/failed-plugin/failed-plugin.php', + 'line' => '3', + ), + 'expected_return' => true, + 'expected_message' => 'PHP Fatal error: Uncaught Exception: Could not connect to the database in wp-content/plugins/failed-plugin/failed-plugin.php:3 in wp-content/plugins/failed-plugin/failed-plugin.php on line 3', + ), + 'fatal error without a stack trace' => array( + 'scrape_result' => array( + 'type' => '64', + 'message' => 'Cannot redeclare failed_plugin_init()', + 'file' => 'wp-content/plugins/failed-plugin/failed-plugin.php', + 'line' => '12', + ), + 'expected_return' => true, + 'expected_message' => 'PHP Fatal error: Cannot redeclare failed_plugin_init() in wp-content/plugins/failed-plugin/failed-plugin.php on line 12', + ), + 'no fatal error' => array( + 'scrape_result' => true, + 'expected_return' => false, + 'expected_message' => null, + ), + 'failed loopback request' => array( + 'scrape_result' => new WP_Error( 'http_request_failed', 'cURL error 28: Operation timed out' ), + 'expected_return' => true, + 'expected_message' => 'The loopback request to check for fatal errors failed: cURL error 28: Operation timed out', + ), + ); + } } diff --git a/tests/phpunit/tests/admin/wpAutomaticUpdaterShouldUpdate.php b/tests/phpunit/tests/admin/wpAutomaticUpdaterShouldUpdate.php new file mode 100644 index 0000000000000..06d78555f0010 --- /dev/null +++ b/tests/phpunit/tests/admin/wpAutomaticUpdaterShouldUpdate.php @@ -0,0 +1,466 @@ +credential_requests[] = array( $context, $allow_relaxed_file_ownership ); + + return $this->credentials_available; + } + + /** + * Gets an updater that records whether a core update notification email would be sent. + * + * @param int $email_count Expected number of core update notification emails. + * @return WP_Automatic_Updater&PHPUnit\Framework\MockObject\MockObject Updater. + */ + private function get_updater( $email_count = 0 ) { + $updater = $this->getMockBuilder( WP_Automatic_Updater::class ) + // Note: setMethods() is deprecated in PHPUnit 9, but still supported. + ->setMethods( array( 'send_core_update_notification_email' ) ) + ->getMock(); + + $updater->expects( $this->exactly( $email_count ) )->method( 'send_core_update_notification_email' ); + + return $updater; + } + + /** + * Gets a plugin update offer. + * + * @param array $fields Fields to override. + * @return object Update offer. + */ + private static function plugin_offer( array $fields = array() ) { + return (object) array_merge( + array( + 'plugin' => 'my-plugin/my-plugin.php', + 'new_version' => '2.0', + ), + $fields + ); + } + + /** + * Gets a core update offer that is compatible with the current environment. + * + * @param array $fields Fields to override. + * @return object Update offer. + */ + private static function core_offer( array $fields = array() ) { + return (object) array_merge( + array( + 'current' => '99.0', + 'php_version' => '5.6', + 'mysql_version' => '5.0', + 'new_files' => true, + ), + $fields + ); + } + + /** + * Tests that nothing is updated while background updates are disabled. + * + * @ticket 65819 + */ + public function test_returns_false_when_updates_are_disabled() { + add_filter( 'automatic_updater_disabled', '__return_true', 20 ); + + $this->assertFalse( $this->get_updater()->should_update( 'plugin', self::plugin_offer( array( 'autoupdate' => true ) ), WP_PLUGIN_DIR ) ); + $this->assertSame( array(), $this->credential_requests, 'The filesystem should not be checked.' ); + } + + /** + * Tests that nothing is updated when filesystem credentials are not available. + * + * @ticket 65819 + */ + public function test_returns_false_without_filesystem_credentials() { + $this->credentials_available = false; + + $this->assertFalse( $this->get_updater()->should_update( 'plugin', self::plugin_offer( array( 'autoupdate' => true ) ), WP_PLUGIN_DIR ) ); + $this->assertSame( array( array( WP_PLUGIN_DIR, false ) ), $this->credential_requests, 'The filesystem should be checked for the context.' ); + } + + /** + * Tests that nothing is updated in a version-controlled checkout. + * + * @ticket 65819 + */ + public function test_returns_false_for_vcs_checkout() { + add_filter( 'automatic_updates_is_vcs_checkout', '__return_true', 20 ); + + $this->assertFalse( $this->get_updater()->should_update( 'plugin', self::plugin_offer( array( 'autoupdate' => true ) ), WP_PLUGIN_DIR ) ); + } + + /** + * Tests that a core update notification email is sent when core cannot be updated in the background. + * + * @ticket 65819 + * + * @dataProvider data_core_updates_that_cannot_run + * + * @param bool $credentials_available Whether filesystem credentials are available. + * @param bool $is_vcs_checkout Whether the site is a version-controlled checkout. + */ + public function test_sends_core_notification_when_core_cannot_be_updated( $credentials_available, $is_vcs_checkout ) { + $this->credentials_available = $credentials_available; + + if ( $is_vcs_checkout ) { + add_filter( 'automatic_updates_is_vcs_checkout', '__return_true', 20 ); + } + + $this->assertFalse( $this->get_updater( 1 )->should_update( 'core', self::core_offer(), ABSPATH ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_core_updates_that_cannot_run() { + return array( + 'no filesystem credentials' => array( false, false ), + 'version-controlled site' => array( true, true ), + ); + } + + /** + * Tests that relaxed file ownership is only allowed for core updates without new files. + * + * @ticket 65819 + * + * @dataProvider data_relaxed_file_ownership + * + * @param string $type Update type. + * @param object $item Update offer. + * @param bool $expected Whether relaxed file ownership should be allowed. + */ + public function test_allows_relaxed_file_ownership_only_for_core_without_new_files( $type, $item, $expected ) { + add_filter( "auto_update_{$type}", '__return_false' ); + + $this->get_updater( 'core' === $type ? 1 : 0 )->should_update( $type, $item, ABSPATH ); + + $this->assertSame( array( array( ABSPATH, $expected ) ), $this->credential_requests ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_relaxed_file_ownership() { + return array( + 'core without new files' => array( 'core', self::core_offer( array( 'new_files' => false ) ), true ), + 'core with new files' => array( 'core', self::core_offer( array( 'new_files' => true ) ), false ), + 'core without the new files flag' => array( 'core', (object) array( 'current' => '99.0' ), false ), + 'plugin without new files' => array( 'plugin', self::plugin_offer( array( 'new_files' => false ) ), false ), + ); + } + + /** + * Tests whether plugins and themes are updated, from their offer and the site's auto-update settings. + * + * @ticket 65819 + * + * @dataProvider data_plugin_and_theme_updates + * + * @param string $type Update type, 'plugin' or 'theme'. + * @param array $fields Fields of the update offer. + * @param string[] $auto_updates Items with auto-updates enabled by the site admin. + * @param bool $type_enabled Whether auto-updates are enabled for the type. + * @param bool $expected Whether the item should be updated. + */ + public function test_plugin_and_theme_updates( $type, $fields, $auto_updates, $type_enabled, $expected ) { + update_site_option( "auto_update_{$type}s", $auto_updates ); + add_filter( "{$type}s_auto_update_enabled", $type_enabled ? '__return_true' : '__return_false' ); + + $item = (object) array_merge( array( $type => "my-{$type}" ), $fields ); + + $this->assertSame( $expected, $this->get_updater()->should_update( $type, $item, WP_CONTENT_DIR ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_plugin_and_theme_updates() { + return array( + 'plugin with the autoupdate flag' => array( 'plugin', array( 'autoupdate' => true ), array(), false, true ), + 'plugin without the autoupdate flag' => array( 'plugin', array(), array(), true, false ), + 'plugin with a falsy autoupdate flag' => array( 'plugin', array( 'autoupdate' => 0 ), array(), true, false ), + 'plugin enabled by the site admin' => array( 'plugin', array(), array( 'my-plugin' ), true, true ), + 'plugin enabled while auto-updates are off' => array( 'plugin', array(), array( 'my-plugin' ), false, false ), + 'other plugin enabled by the site admin' => array( 'plugin', array(), array( 'other-plugin' ), true, false ), + 'theme enabled by the site admin' => array( 'theme', array(), array( 'my-theme' ), true, true ), + 'theme with the autoupdate flag' => array( 'theme', array( 'autoupdate' => true ), array(), false, true ), + 'theme without the autoupdate flag' => array( 'theme', array(), array(), true, false ), + ); + } + + /** + * Tests that translations are updated when the offer allows it. + * + * @ticket 65819 + * + * @dataProvider data_translation_updates + * + * @param array $fields Fields of the update offer. + * @param bool $expected Whether the translation should be updated. + */ + public function test_translation_updates( $fields, $expected ) { + $item = (object) array_merge( array( 'language' => 'de_DE' ), $fields ); + + $this->assertSame( $expected, $this->get_updater()->should_update( 'translation', $item, WP_LANG_DIR ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_translation_updates() { + return array( + 'with the autoupdate flag' => array( array( 'autoupdate' => true ), true ), + 'without the autoupdate flag' => array( array(), false ), + 'with a falsy autoupdate flag' => array( array( 'autoupdate' => false ), false ), + ); + } + + /** + * Tests that the disable_autoupdate flag overrides the user's choice, but not the filter. + * + * @ticket 65819 + */ + public function test_disable_autoupdate_flag_overrides_choice_but_not_filter() { + $item = self::plugin_offer( + array( + 'autoupdate' => true, + 'disable_autoupdate' => true, + ) + ); + + $this->assertFalse( $this->get_updater()->should_update( 'plugin', $item, WP_PLUGIN_DIR ), 'The flag should prevent the update.' ); + + add_filter( 'auto_update_plugin', '__return_true' ); + + $this->assertTrue( $this->get_updater()->should_update( 'plugin', $item, WP_PLUGIN_DIR ), 'The filter should still allow the update.' ); + } + + /** + * Tests that the auto_update_{$type} filter receives the decision and the offer, and overrides it. + * + * @ticket 65819 + * + * @dataProvider data_filter_values + * + * @param mixed $filtered Value returned by the filter. + * @param bool $expected Whether the item should be updated. + */ + public function test_auto_update_filter( $filtered, $expected ) { + $item = self::plugin_offer( array( 'autoupdate' => true ) ); + + $filter = new MockAction(); + add_filter( 'auto_update_plugin', array( $filter, 'filter' ), 10, 2 ); + add_filter( + 'auto_update_plugin', + static function () use ( $filtered ) { + return $filtered; + }, + 20 + ); + + $this->assertSame( $expected, $this->get_updater()->should_update( 'plugin', $item, WP_PLUGIN_DIR ), 'The filtered decision should be used.' ); + $this->assertSame( array( array( true, $item ) ), $filter->get_args(), 'The filter should receive the decision and the offer.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_filter_values() { + return array( + 'true' => array( true, true ), + 'false' => array( false, false ), + 'null' => array( null, false ), + 'zero' => array( 0, false ), + 'empty string' => array( '', false ), + 'truthy value' => array( 1, true ), + ); + } + + /** + * Tests that a core update notification email is sent when the filter declines the core update. + * + * @ticket 65819 + */ + public function test_sends_core_notification_when_core_update_is_declined() { + add_filter( 'auto_update_core', '__return_false' ); + + $this->assertFalse( $this->get_updater( 1 )->should_update( 'core', self::core_offer(), ABSPATH ) ); + } + + /** + * Tests that the core update decision comes from the core update policy when no auto_update_core filter is added. + * + * @ticket 65819 + * + * @dataProvider data_core_update_policy + * + * @param bool $allow_major Whether the core update policy allows major updates. + * @param bool $expected Whether core should be updated. + */ + public function test_core_update_follows_core_update_policy( $allow_major, $expected ) { + add_filter( 'allow_dev_auto_core_updates', '__return_true' ); + add_filter( 'allow_major_auto_core_updates', $allow_major ? '__return_true' : '__return_false' ); + + $this->assertSame( $expected, $this->get_updater( $expected ? 0 : 1 )->should_update( 'core', self::core_offer(), ABSPATH ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_core_update_policy() { + return array( + 'policy allows the update' => array( true, true ), + 'policy denies the update' => array( false, false ), + ); + } + + /** + * Tests the PHP and MySQL requirements of core updates. + * + * @ticket 65819 + * + * @dataProvider data_core_requirements + * + * @param array $fields Fields of the update offer. + * @param bool $expected Whether core should be updated. + */ + public function test_core_update_requirements( $fields, $expected ) { + add_filter( 'auto_update_core', '__return_true' ); + + $this->assertSame( $expected, $this->get_updater()->should_update( 'core', self::core_offer( $fields ), ABSPATH ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_core_requirements() { + return array( + 'requirements met' => array( array(), true ), + 'PHP version too old' => array( array( 'php_version' => '99.0' ), false ), + 'MySQL version too old' => array( array( 'mysql_version' => '99.0' ), false ), + ); + } + + /** + * Tests the PHP requirement of plugin and theme updates. + * + * @ticket 65819 + * + * @dataProvider data_plugin_and_theme_php_requirements + * + * @param string $type Update type, 'plugin' or 'theme'. + * @param mixed $requires_php Required PHP version of the update. + * @param bool $expected Whether the item should be updated. + */ + public function test_plugin_and_theme_php_requirements( $type, $requires_php, $expected ) { + $item = (object) array( + $type => "my-{$type}", + 'autoupdate' => true, + 'requires_php' => $requires_php, + ); + + $this->assertSame( $expected, $this->get_updater()->should_update( $type, $item, WP_CONTENT_DIR ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_plugin_and_theme_php_requirements() { + return array( + 'plugin, requirement met' => array( 'plugin', '5.6', true ), + 'plugin, PHP too old' => array( 'plugin', '99.0', false ), + 'plugin, no requirement' => array( 'plugin', '', true ), + 'plugin, null requirement' => array( 'plugin', null, true ), + 'theme, PHP too old' => array( 'theme', '99.0', false ), + 'theme, requirement met' => array( 'theme', '5.6', true ), + ); + } + + /** + * Tests that the PHP requirement does not apply to translations. + * + * @ticket 65819 + */ + public function test_php_requirement_does_not_apply_to_translations() { + $item = (object) array( + 'language' => 'de_DE', + 'autoupdate' => true, + 'requires_php' => '99.0', + ); + + $this->assertTrue( $this->get_updater()->should_update( 'translation', $item, WP_LANG_DIR ) ); + } +} diff --git a/tests/phpunit/tests/admin/wpColorSchemeMenuIconStyles.php b/tests/phpunit/tests/admin/wpColorSchemeMenuIconStyles.php new file mode 100644 index 0000000000000..85daae297deed --- /dev/null +++ b/tests/phpunit/tests/admin/wpColorSchemeMenuIconStyles.php @@ -0,0 +1,112 @@ +original_admin_css_colors = $GLOBALS['_wp_admin_css_colors'] ?? null; + $GLOBALS['wp_styles'] = null; + + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + } + + public function tear_down() { + $GLOBALS['_wp_admin_css_colors'] = $this->original_admin_css_colors; + $GLOBALS['wp_styles'] = null; + + parent::tear_down(); + } + + /** + * Registers a color scheme and makes it the scheme of the current user. + * + * @param string $url URL of the stylesheet of the scheme. + * @param array $icons Icon colors of the scheme. + */ + private function use_color_scheme( $url, $icons ): void { + wp_admin_css_color( 'test-scheme', 'Test scheme', $url, array( '#111111', '#222222' ), $icons ); + update_user_option( get_current_user_id(), 'admin_color', 'test-scheme', true ); + } + + /** + * A third-party scheme gets its icon colors on SVG menu icons. + * + * @ticket 65089 + */ + public function test_adds_icon_colors_of_third_party_scheme(): void { + $this->use_color_scheme( + 'https://example.org/scheme.css', + array( + 'base' => '#aaaaaa', + 'focus' => '#bbbbbb', + 'current' => '#cccccc', + ) + ); + + wp_color_scheme_menu_icon_styles(); + + $styles = implode( '', (array) wp_styles()->get_data( 'admin-menu', 'after' ) ); + + $this->assertStringContainsString( 'color:#aaaaaa', $styles ); + $this->assertStringContainsString( 'color:#bbbbbb', $styles ); + $this->assertStringContainsString( 'color:#cccccc', $styles ); + } + + /** + * @ticket 65089 + * + * @dataProvider data_schemes_without_icon_styles + * + * @param string $url URL of the stylesheet of the scheme. + * @param array $icons Icon colors of the scheme. + */ + public function test_adds_nothing_for_other_schemes( $url, $icons ): void { + $this->use_color_scheme( $url, $icons ); + + wp_color_scheme_menu_icon_styles(); + + $this->assertEmpty( wp_styles()->get_data( 'admin-menu', 'after' ) ); + } + + /** + * Data provider. + * + * @return array }> + */ + public static function data_schemes_without_icon_styles(): array { + $icons = array( + 'base' => '#aaaaaa', + 'focus' => '#bbbbbb', + 'current' => '#cccccc', + ); + + return array( + 'core scheme' => array( admin_url( 'css/colors/light/colors.css' ), $icons ), + 'no stylesheet' => array( '', $icons ), + ); + } +} diff --git a/tests/phpunit/tests/admin/wpCommentsListTable.php b/tests/phpunit/tests/admin/wpCommentsListTable.php index 185bc5bfa48b0..b1bbce346328a 100644 --- a/tests/phpunit/tests/admin/wpCommentsListTable.php +++ b/tests/phpunit/tests/admin/wpCommentsListTable.php @@ -215,27 +215,30 @@ public function test_get_views_should_return_views_by_default() { } /** - * Verify that the comments table never shows the note comment_type. + * Verify that the comments table never shows internal comment types. * * @ticket 64198 * @ticket 64474 + * @ticket 64638 * * @dataProvider data_comment_type * * @param string $comment_type The comment_type parameter value to test. */ - public function test_comments_list_table_does_not_show_note_comment_type( string $comment_type ) { + public function test_comments_list_table_does_not_show_internal_comment_types( string $comment_type ) { $post_id = self::factory()->post->create(); - self::factory()->comment->create( - array( - 'comment_post_ID' => $post_id, - 'comment_content' => 'This is a note.', - 'comment_type' => 'note', - 'comment_approved' => '1', - 'comment_date' => '2024-01-01 10:00:00', - 'comment_date_gmt' => '2024-01-01 10:00:00', - ) - ); + foreach ( array( 'note', 'reaction' ) as $internal_type ) { + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_content' => 'This is a ' . $internal_type . '.', + 'comment_type' => $internal_type, + 'comment_approved' => '1', + 'comment_date' => '2024-01-01 10:00:00', + 'comment_date_gmt' => '2024-01-01 10:00:00', + ) + ); + } $regular_comment_id = self::factory()->comment->create( array( 'comment_post_ID' => $post_id, @@ -260,19 +263,20 @@ public function test_comments_list_table_does_not_show_note_comment_type( string $this->table->prepare_items(); $items = $this->table->items; $this->assertCount( 2, $items ); - $this->assertEquals( $pingback_comment_id, $items[0]->comment_ID ); - $this->assertEquals( $regular_comment_id, $items[1]->comment_ID ); + $this->assertSame( (string) $pingback_comment_id, $items[0]->comment_ID ); + $this->assertSame( (string) $regular_comment_id, $items[1]->comment_ID ); } /** - * Data provider for test_comments_list_table_does_not_show_note_comment_type(). + * Data provider for test_comments_list_table_does_not_show_internal_comment_types(). * * @return array */ - public function data_comment_type(): array { + public static function data_comment_type(): array { return array( - 'note type explicitly requested' => array( 'note' ), - 'all type requested' => array( 'all' ), + 'note type explicitly requested' => array( 'note' ), + 'reaction type explicitly requested' => array( 'reaction' ), + 'all type requested' => array( 'all' ), ); } } diff --git a/tests/phpunit/tests/admin/wpCommunityEvents.php b/tests/phpunit/tests/admin/wpCommunityEvents.php index fd44a86e272a4..a38351b5fbece 100644 --- a/tests/phpunit/tests/admin/wpCommunityEvents.php +++ b/tests/phpunit/tests/admin/wpCommunityEvents.php @@ -573,7 +573,7 @@ public function test_get_unsafe_client_ip( $raw_ip, $expected_result ) { * * @return array */ - public function data_get_unsafe_client_ip() { + public static function data_get_unsafe_client_ip() { return array( // Handle '::' returned from `wp_privacy_anonymize_ip()`. array( diff --git a/tests/phpunit/tests/admin/wpDashboardSiteActivity.php b/tests/phpunit/tests/admin/wpDashboardSiteActivity.php new file mode 100644 index 0000000000000..9d5f673afc7d6 --- /dev/null +++ b/tests/phpunit/tests/admin/wpDashboardSiteActivity.php @@ -0,0 +1,413 @@ +user->create( + array( + 'display_name' => 'Current Writer', + 'role' => 'author', + ) + ); + self::$other_user_id = $factory->user->create( + array( + 'display_name' => 'Guest Writer', + 'role' => 'author', + ) + ); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$user_id ); + self::delete_user( self::$other_user_id ); + } + + public function set_up() { + parent::set_up(); + + set_current_screen( 'dashboard' ); + wp_set_current_user( self::$user_id ); + $_SERVER['REQUEST_METHOD'] = 'GET'; + } + + public function tear_down() { + delete_option( 'dashboard_recent_comments_widget_registered' ); + unset( $GLOBALS['wp_meta_boxes']['dashboard'] ); + + parent::tear_down(); + } + + /** + * Creates a published post on today's calendar day in a previous year. + * + * @param int $author_id Author ID. + * @param string $title Post title. + * @param int $years_ago Number of years before today. + * @param array $post_args Additional post arguments. + * @return int Post ID. + */ + private function create_matching_post( $author_id, $title = 'A memory from last year', $years_ago = 1, $post_args = array() ) { + $today = current_datetime(); + + if ( '02-29' === $today->format( 'm-d' ) ) { + $years_ago *= 4; + } + + $post_date = ( (int) $today->format( 'Y' ) - $years_ago ) . '-' . $today->format( 'm-d' ) . ' 12:00:00'; + + return self::factory()->post->create( + array_merge( + array( + 'post_author' => $author_id, + 'post_date' => $post_date, + 'post_date_gmt' => get_gmt_from_date( $post_date ), + 'post_status' => 'publish', + 'post_title' => $title, + ), + $post_args + ) + ); + } + + /** + * @ticket 65116 + * + * @covers ::_wp_dashboard_on_this_day_date_query_clause + */ + public function test_date_query_includes_february_29_on_february_28_in_non_leap_year() { + $date = new DateTimeImmutable( '2023-02-28 12:00:00', wp_timezone() ); + + $this->assertSame( + array( + 'relation' => 'OR', + array( + 'month' => 2, + 'day' => 28, + ), + array( + 'month' => 2, + 'day' => 29, + ), + ), + _wp_dashboard_on_this_day_date_query_clause( $date ) + ); + } + + /** + * @ticket 65116 + * + * @covers ::wp_dashboard_on_this_day_get_posts + */ + public function test_query_returns_only_published_posts_from_this_day_in_previous_years() { + $matching_id = $this->create_matching_post( self::$user_id ); + $this->create_matching_post( self::$user_id, 'A draft memory', 2, array( 'post_status' => 'draft' ) ); + + $today = current_datetime(); + $current_day = $today->format( 'Y-m-d' ) . ' 12:00:00'; + self::factory()->post->create( + array( + 'post_author' => self::$user_id, + 'post_date' => $current_day, + 'post_status' => 'publish', + 'post_title' => 'A memory from this year', + ) + ); + + $nearby_date = $today->modify( '-1 year +1 day' )->format( 'Y-m-d' ) . ' 12:00:00'; + self::factory()->post->create( + array( + 'post_author' => self::$user_id, + 'post_date' => $nearby_date, + 'post_status' => 'publish', + 'post_title' => 'A nearby memory', + ) + ); + + $posts = wp_dashboard_on_this_day_get_posts(); + + $this->assertSame( array( $matching_id ), wp_list_pluck( $posts, 'ID' ) ); + } + + /** + * @ticket 65116 + * + * @covers ::wp_dashboard_on_this_day_get_posts + */ + public function test_query_defaults_to_ten_posts_and_limit_is_filterable() { + for ( $years_ago = 1; $years_ago <= 11; $years_ago++ ) { + $this->create_matching_post( self::$user_id, 'Anniversary post ' . $years_ago, $years_ago ); + } + + $this->assertCount( 10, wp_dashboard_on_this_day_get_posts() ); + + add_filter( 'wp_dashboard_on_this_day_query_args', array( $this, 'filter_on_this_day_query_limit' ) ); + try { + $this->assertCount( 3, wp_dashboard_on_this_day_get_posts() ); + } finally { + remove_filter( 'wp_dashboard_on_this_day_query_args', array( $this, 'filter_on_this_day_query_limit' ) ); + } + } + + /** + * @ticket 65116 + * + * @covers ::wp_dashboard_on_this_day + */ + public function test_section_renders_posts_from_all_authors_and_untitled_excerpts() { + $this->create_matching_post( self::$user_id, 'A note from me' ); + $this->create_matching_post( self::$other_user_id, 'A note from someone else', 2 ); + $this->create_matching_post( + self::$user_id, + '', + 3, + array( 'post_excerpt' => 'word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12 word13 word14 word15 word16' ) + ); + + ob_start(); + $result = wp_dashboard_on_this_day(); + $output = ob_get_clean(); + + $this->assertTrue( $result ); + $this->assertStringContainsString( 'Published On This Day', $output ); + $this->assertStringContainsString( 'A note from me', $output ); + $this->assertStringNotContainsString( 'by Current Writer', $output ); + $this->assertStringContainsString( 'A note from someone else', $output ); + $this->assertStringContainsString( 'by Guest Writer', $output ); + $this->assertStringContainsString( '(no title)', $output ); + $this->assertStringContainsString( 'word15', $output ); + $this->assertStringNotContainsString( 'word16', $output ); + } + + /** + * @ticket 65116 + * + * @covers ::wp_dashboard_on_this_day + */ + public function test_section_hides_untitled_excerpt_for_unreadable_posts() { + $this->create_matching_post( + self::$other_user_id, + '', + 1, + array( + 'post_excerpt' => 'Unreadable private anniversary memory.', + 'post_status' => 'private', + ) + ); + + add_filter( 'wp_dashboard_on_this_day_query_args', array( $this, 'filter_on_this_day_query_private_posts' ) ); + try { + ob_start(); + wp_dashboard_on_this_day(); + $output = ob_get_clean(); + } finally { + remove_filter( 'wp_dashboard_on_this_day_query_args', array( $this, 'filter_on_this_day_query_private_posts' ) ); + } + + $this->assertStringContainsString( '(no title)', $output ); + $this->assertStringNotContainsString( 'Unreadable private anniversary memory.', $output ); + } + + /** + * @ticket 65116 + * + * @covers ::wp_dashboard_on_this_day + */ + public function test_section_hides_untitled_excerpt_for_password_protected_posts() { + $this->create_matching_post( + self::$user_id, + '', + 1, + array( + 'post_excerpt' => 'Password-protected anniversary memory.', + 'post_password' => 'secret', + ) + ); + + ob_start(); + wp_dashboard_on_this_day(); + $output = ob_get_clean(); + + $this->assertStringContainsString( '(no title)', $output ); + $this->assertStringNotContainsString( 'Password-protected anniversary memory.', $output ); + } + + /** + * @ticket 65116 + * + * @covers ::wp_dashboard_site_activity + */ + public function test_activity_renders_no_activity_message_without_posts() { + ob_start(); + wp_dashboard_site_activity(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'No activity yet!', $output ); + $this->assertStringNotContainsString( 'Published On This Day', $output ); + } + + /** + * @ticket 65116 + * + * @covers ::wp_dashboard_site_activity + * @covers ::wp_dashboard_recent_posts + */ + public function test_activity_does_not_repeat_on_this_day_posts_in_recently_published() { + $this->create_matching_post( self::$user_id, 'One anniversary mention' ); + self::factory()->post->create( + array( + 'post_author' => self::$user_id, + 'post_status' => 'publish', + 'post_title' => 'A recent post', + ) + ); + + ob_start(); + wp_dashboard_site_activity(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'Published On This Day', $output ); + $this->assertSame( 1, preg_match( '/
    /s', $output, $recent_posts ) ); + $this->assertStringNotContainsString( 'One anniversary mention', $recent_posts[0] ); + } + + /** + * @ticket 65116 + * + * @covers ::wp_dashboard_site_activity + * @covers ::wp_dashboard_recent_comments_widget + * @covers ::wp_dashboard_recent_comments + */ + public function test_recent_comments_are_rendered_only_in_their_dedicated_widget() { + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$user_id, + 'post_status' => 'draft', + ) + ); + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_content' => 'A comment in its own widget.', + ) + ); + + ob_start(); + wp_dashboard_site_activity(); + $activity_output = ob_get_clean(); + + ob_start(); + wp_dashboard_recent_comments_widget(); + $comments_output = ob_get_clean(); + + ob_start(); + wp_dashboard_recent_comments(); + $direct_output = ob_get_clean(); + + $this->assertStringNotContainsString( 'A comment in its own widget.', $activity_output ); + $this->assertStringContainsString( 'No activity yet!', $activity_output ); + $this->assertStringContainsString( 'A comment in its own widget.', $comments_output ); + $this->assertStringNotContainsString( '

    Recent Comments

    ', $comments_output ); + $this->assertStringContainsString( '

    Recent Comments

    ', $direct_output ); + } + + /** + * @ticket 65116 + * + * @covers ::wp_dashboard_recent_comments_widget + */ + public function test_recent_comments_widget_renders_empty_state_when_user_has_no_visible_comments() { + $post_id = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_approved' => '0', + ) + ); + wp_set_current_user( self::factory()->user->create( array( 'role' => 'subscriber' ) ) ); + + ob_start(); + wp_dashboard_recent_comments_widget(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'No comments to show.', $output ); + } + + /** + * @ticket 65116 + * + * @covers ::_wp_dashboard_has_received_comments + * @covers ::wp_dashboard_setup + */ + public function test_recent_comments_widget_remains_registered_after_comments_are_deleted() { + add_filter( 'pre_http_request', array( $this, 'mock_http_request_failure' ) ); + + try { + wp_dashboard_setup(); + $this->assertArrayNotHasKey( 'dashboard_recent_comments', $GLOBALS['wp_meta_boxes']['dashboard']['normal']['core'] ); + + unset( $GLOBALS['wp_meta_boxes']['dashboard'] ); + + $post_id = self::factory()->post->create(); + $comment_id = self::factory()->comment->create( array( 'comment_post_ID' => $post_id ) ); + + wp_dashboard_setup(); + $this->assertArrayHasKey( 'dashboard_recent_comments', $GLOBALS['wp_meta_boxes']['dashboard']['normal']['core'] ); + $this->assertSame( + 'wp_dashboard_recent_comments_widget', + $GLOBALS['wp_meta_boxes']['dashboard']['normal']['core']['dashboard_recent_comments']['callback'] + ); + $this->assertSame( 1, get_option( 'dashboard_recent_comments_widget_registered' ) ); + + wp_delete_comment( $comment_id, true ); + unset( $GLOBALS['wp_meta_boxes']['dashboard'] ); + + wp_dashboard_setup(); + $this->assertArrayHasKey( 'dashboard_recent_comments', $GLOBALS['wp_meta_boxes']['dashboard']['normal']['core'] ); + } finally { + remove_filter( 'pre_http_request', array( $this, 'mock_http_request_failure' ) ); + } + } + + /** + * Mocks an HTTP request failure during dashboard setup. + * + * @return WP_Error Mock request error. + */ + public function mock_http_request_failure() { + return new WP_Error( 'http_request_failed' ); + } + + /** + * Sets the On This Day query limit to three. + * + * @param array $args Query arguments. + * @return array Filtered query arguments. + */ + public function filter_on_this_day_query_limit( $args ) { + $args['posts_per_page'] = 3; + + return $args; + } + + /** + * Filters the On This Day query to include private posts. + * + * @param array $args Query arguments. + * @return array Filtered query arguments. + */ + public function filter_on_this_day_query_private_posts( $args ) { + $args['post_status'] = array( 'private' ); + + return $args; + } +} diff --git a/tests/phpunit/tests/admin/wpDebugDataPlugins.php b/tests/phpunit/tests/admin/wpDebugDataPlugins.php new file mode 100644 index 0000000000000..d4a9e848473da --- /dev/null +++ b/tests/phpunit/tests/admin/wpDebugDataPlugins.php @@ -0,0 +1,446 @@ +created_files as $file ) { + unlink( $file ); + } + $this->created_files = array(); + + wp_cache_delete( 'plugins', 'plugins' ); + delete_site_transient( 'update_plugins' ); + + parent::tear_down(); + } + + /** + * Tests the must-use plugins section without must-use plugins. + * + * @ticket 65819 + */ + public function test_mu_plugins_should_return_empty_section() { + $this->assertSame( + array( + 'label' => 'Must Use Plugins', + 'show_count' => true, + 'fields' => array(), + ), + $this->call( 'get_wp_mu_plugins' ) + ); + } + + /** + * Tests the version and author strings of a must-use plugin. + * + * @ticket 65819 + * + * @dataProvider data_version_strings + * + * @param string $version The Version header. + * @param string $author The Author header. + * @param string $value The expected value. + * @param string $debug The expected debug value. + */ + public function test_mu_plugins_should_describe_version_and_author( $version, $author, $value, $debug ) { + $headers = " * Plugin Name: My MU Plugin\n"; + if ( '' !== $version ) { + $headers .= " * Version: {$version}\n"; + } + if ( '' !== $author ) { + $headers .= " * Author: {$author}\n"; + } + + $this->create_mu_plugin( 'my-mu-plugin.php', "assertSame( + array( + 'My MU Plugin' => array( + 'label' => 'My MU Plugin', + 'value' => $value, + 'debug' => $debug, + ), + ), + $this->call( 'get_wp_mu_plugins' )['fields'] + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_version_strings() { + return array( + 'version and author' => array( '1.0', 'Jane', 'Version 1.0 by Jane', 'version: 1.0, author: Jane' ), + 'author only' => array( '', 'Jane', 'By Jane', 'author: Jane, version: (undefined)' ), + 'version only' => array( '1.0', '', 'Version 1.0', 'author: (undefined), version: 1.0' ), + 'neither' => array( '', '', 'No version or author information is available.', 'author: (undefined), version: (undefined)' ), + ); + } + + /** + * Tests the version and author strings of a plugin, and its split into active and inactive. + * + * @ticket 65819 + * + * @dataProvider data_version_strings + * + * @param string $version The Version header. + * @param string $author The Author header. + * @param string $value The expected value. + * @param string $debug The expected debug value. + */ + public function test_plugins_should_describe_version_and_author( $version, $author, $value, $debug ) { + $this->set_plugins( + array( + 'active/active.php' => array( 'Active Plugin', $version, $author ), + 'inactive/inactive.php' => array( 'Inactive Plugin', $version, $author ), + ), + array( 'active/active.php' ) + ); + + $this->assertSame( + array( + 'wp-plugins-active' => array( + 'Active Plugin' => array( + 'label' => 'Active Plugin', + 'value' => $value, + 'debug' => $debug, + ), + ), + 'wp-plugins-inactive' => array( + 'Inactive Plugin' => array( + 'label' => 'Inactive Plugin', + 'value' => $value, + 'debug' => $debug, + ), + ), + ), + $this->call( 'get_wp_plugins_raw_data' ) + ); + } + + /** + * Tests that an available update is mentioned. + * + * @ticket 65819 + */ + public function test_plugins_should_mention_latest_version() { + $this->set_plugins( array( 'my-plugin/my-plugin.php' => array( 'My Plugin', '1.0', 'Jane' ) ) ); + $this->set_update( 'response', 'my-plugin/my-plugin.php', '2.0' ); + + $this->assertSame( + array( + 'label' => 'My Plugin', + 'value' => 'Version 1.0 by Jane (Latest version: 2.0)', + 'debug' => 'version: 1.0, author: Jane (latest version: 2.0)', + ), + $this->call( 'get_wp_plugins_raw_data' )['wp-plugins-inactive']['My Plugin'] + ); + } + + /** + * Tests the auto-update status when plugin auto-updates are enabled. + * + * @ticket 65819 + * + * @dataProvider data_auto_update_states + * + * @param bool $in_setting Whether the plugin is in the auto_update_plugins setting. + * @param bool|null $forced The value of the auto_update_plugin filter, or null for no filter. + * @param string $status The expected status string. + */ + public function test_plugins_should_add_auto_update_status( $in_setting, $forced, $status ) { + add_filter( 'plugins_auto_update_enabled', '__return_true', 11 ); + + $this->set_plugins( array( 'my-plugin/my-plugin.php' => array( 'My Plugin', '1.0', 'Jane' ) ) ); + + if ( $in_setting ) { + update_site_option( 'auto_update_plugins', array( 'my-plugin/my-plugin.php' ) ); + } + + if ( null !== $forced ) { + add_filter( 'auto_update_plugin', $forced ? '__return_true' : '__return_false' ); + } + + $this->assertSame( + array( + 'label' => 'My Plugin', + 'value' => 'Version 1.0 by Jane | ' . $status, + 'debug' => 'version: 1.0, author: Jane, ' . $status, + ), + $this->call( 'get_wp_plugins_raw_data' )['wp-plugins-inactive']['My Plugin'] + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_auto_update_states() { + return array( + 'in setting' => array( true, null, 'Auto-updates enabled' ), + 'not in setting' => array( false, null, 'Auto-updates disabled' ), + 'forced on, not in setting' => array( false, true, 'Auto-updates enabled' ), + 'forced off, in setting' => array( true, false, 'Auto-updates disabled' ), + ); + } + + /** + * Tests that the auto-update setting is ignored when plugin auto-updates are disabled. + * + * @ticket 65819 + */ + public function test_plugins_should_ignore_auto_update_setting_when_disabled() { + $this->set_plugins( array( 'my-plugin/my-plugin.php' => array( 'My Plugin', '1.0', 'Jane' ) ) ); + update_site_option( 'auto_update_plugins', array( 'my-plugin/my-plugin.php' ) ); + + $filter = new MockAction(); + add_filter( 'auto_update_plugin', array( $filter, 'filter' ) ); + + $this->assertSame( 'Version 1.0 by Jane', $this->call( 'get_wp_plugins_raw_data' )['wp-plugins-inactive']['My Plugin']['value'], 'No auto-update status should be added.' ); + $this->assertSame( 0, $filter->get_call_count(), 'The auto-update filter should not run.' ); + } + + /** + * Tests the update item passed to the auto-update filter. + * + * @ticket 65819 + * + * @dataProvider data_update_items + * + * @param string $transient_list The transient list holding the plugin, or an empty string for none. + * @param string $new_version The expected new version of the item. + * @param string $name The expected plugin name in the item, or an empty string for none. + */ + public function test_plugins_should_pass_update_item_to_auto_update_filter( $transient_list, $new_version, $name ) { + add_filter( 'plugins_auto_update_enabled', '__return_true', 11 ); + + $this->set_plugins( array( 'my-plugin/my-plugin.php' => array( 'My Plugin', '1.0', 'Jane' ) ) ); + + if ( '' !== $transient_list ) { + $this->set_update( $transient_list, 'my-plugin/my-plugin.php', '2.0' ); + } + + $filter = new MockAction(); + add_filter( 'auto_update_plugin', array( $filter, 'filter' ), 10, 2 ); + + $this->call( 'get_wp_plugins_raw_data' ); + + $args = $filter->get_args(); + $item = $args[0][1]; + + $this->assertNull( $args[0][0], 'The filter should receive null.' ); + $this->assertSame( 'my-plugin/my-plugin.php', $item->plugin, 'The item should be for the plugin.' ); + $this->assertSame( $new_version, $item->new_version, 'The item should hold the expected version.' ); + $this->assertSame( $name, $item->Name ?? '', 'The item should only include the plugin data when built from it.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_update_items() { + return array( + 'available update' => array( 'response', '2.0', '' ), + 'no update' => array( 'no_update', '2.0', '' ), + 'not checked yet' => array( '', '', 'My Plugin' ), + ); + } + + /** + * Tests that the auto-update status string can be filtered. + * + * @ticket 65819 + */ + public function test_plugins_should_filter_auto_update_string() { + add_filter( 'plugins_auto_update_enabled', '__return_true', 11 ); + + $this->set_plugins( array( 'my-plugin/my-plugin.php' => array( 'My Plugin', '1.0', 'Jane' ) ) ); + update_site_option( 'auto_update_plugins', array( 'my-plugin/my-plugin.php' ) ); + + $filter = new MockAction(); + add_filter( 'plugin_auto_update_debug_string', array( $filter, 'filter' ), 10, 4 ); + add_filter( + 'plugin_auto_update_debug_string', + static function () { + return 'Custom'; + }, + 11 + ); + + $field = $this->call( 'get_wp_plugins_raw_data' )['wp-plugins-inactive']['My Plugin']; + $args = $filter->get_args(); + + $this->assertSame( 'Version 1.0 by Jane | Custom', $field['value'], 'The filtered string should be used.' ); + $this->assertSame( 'Auto-updates enabled', $args[0][0], 'The filter should receive the status string.' ); + $this->assertSame( 'my-plugin/my-plugin.php', $args[0][1], 'The filter should receive the plugin file.' ); + $this->assertSame( 'My Plugin', $args[0][2]['Name'], 'The filter should receive the plugin data.' ); + $this->assertTrue( $args[0][3], 'The filter should receive whether auto-updates are enabled.' ); + } + + /** + * Calls a private static method of WP_Debug_Data. + * + * @param string $method The method name. + * @return array The method's return value. + */ + private function call( $method ) { + $reflection = new ReflectionMethod( 'WP_Debug_Data', $method ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection->setAccessible( true ); + } + + return $reflection->invoke( null ); + } + + /** + * Creates a must-use plugin file. + * + * @param string $name The file name. + * @param string $contents The file contents. + */ + private function create_mu_plugin( $name, $contents ) { + $file = WPMU_PLUGIN_DIR . '/' . $name; + + if ( ! is_dir( WPMU_PLUGIN_DIR ) ) { + $this->assertTrue( mkdir( WPMU_PLUGIN_DIR ), 'The must-use plugins directory should be created.' ); + } + + $this->assertFileDoesNotExist( $file, 'The must-use plugin should not exist yet.' ); + $this->assertNotFalse( file_put_contents( $file, $contents ), 'The must-use plugin should be written.' ); + + $this->created_files[] = $file; + } + + /** + * Primes the plugins cache with test plugins. + * + * @param array $plugins Name, Version and Author of each plugin, keyed by plugin file. + * @param string[] $active The active plugin files. + */ + private function set_plugins( $plugins, $active = array() ) { + $data = array(); + foreach ( $plugins as $file => $headers ) { + list( $name, $version, $author ) = $headers; + + $data[ $file ] = array( + 'Name' => $name, + 'Version' => $version, + 'Author' => $author, + ); + } + + wp_cache_set( 'plugins', array( '' => $data ), 'plugins' ); + + if ( is_multisite() ) { + update_site_option( 'active_sitewide_plugins', array_fill_keys( $active, time() ) ); + } else { + update_option( 'active_plugins', $active ); + } + } + + /** + * Adds a plugin to a list of the update transient. + * + * @param string $transient_list The transient list, 'response' or 'no_update'. + * @param string $file The plugin file. + * @param string $new_version The new version. + */ + private function set_update( $transient_list, $file, $new_version ) { + $transient = get_site_transient( 'update_plugins' ); + if ( ! is_object( $transient ) ) { + $transient = (object) array( + 'response' => array(), + 'no_update' => array(), + ); + } + + $transient->{$transient_list}[ $file ] = (object) array( + 'plugin' => $file, + 'slug' => dirname( $file ), + 'new_version' => $new_version, + ); + + set_site_transient( 'update_plugins', $transient ); + } + + /** + * Moves existing must-use plugins out of the way. + */ + private static function _back_up_mu_plugins() { + if ( is_dir( WPMU_PLUGIN_DIR ) ) { + rename( WPMU_PLUGIN_DIR, WP_CONTENT_DIR . '/mu-plugin-backup-debug-data' ); + } + } + + /** + * Restores the backed-up must-use plugins. + */ + private static function _restore_mu_plugins() { + $backup = WP_CONTENT_DIR . '/mu-plugin-backup-debug-data'; + + if ( is_dir( WPMU_PLUGIN_DIR ) ) { + rmdir( WPMU_PLUGIN_DIR ); + } + + if ( is_dir( $backup ) ) { + rename( $backup, WPMU_PLUGIN_DIR ); + } + } +} diff --git a/tests/phpunit/tests/admin/wpImporter.php b/tests/phpunit/tests/admin/wpImporter.php new file mode 100644 index 0000000000000..f5620f01d6d29 --- /dev/null +++ b/tests/phpunit/tests/admin/wpImporter.php @@ -0,0 +1,533 @@ +had_argv = array_key_exists( 'argv', $_SERVER ); + $this->orig_argv = $this->had_argv ? $_SERVER['argv'] : null; + } + + public function tear_down() { + if ( $this->had_argv ) { + $_SERVER['argv'] = $this->orig_argv; + } else { + unset( $_SERVER['argv'] ); + } + + parent::tear_down(); + } + + /** + * Tests that imported permalinks are mapped to their post IDs for the given importer and blog. + * + * @ticket 65819 + * + * @covers WP_Importer::get_imported_posts + */ + public function test_get_imported_posts_maps_permalinks_to_post_ids() { + $first = self::factory()->post->create(); + $second = self::factory()->post->create(); + + add_post_meta( $first, 'blogger_7_permalink', '/2026/01/first.html' ); + add_post_meta( $second, 'blogger_7_permalink', '/2026/02/second.html' ); + add_post_meta( $second, 'blogger_8_permalink', '/other-blog.html' ); + add_post_meta( $second, 'other_7_permalink', '/other-importer.html' ); + + $this->assertSame( + array( + '/2026/01/first.html' => $first, + '/2026/02/second.html' => $second, + ), + ( new WP_Importer() )->get_imported_posts( 'blogger', 7 ) + ); + } + + /** + * Tests that imported permalinks are read in chunks of 100 until none are left. + * + * @ticket 65819 + * + * @covers WP_Importer::get_imported_posts + */ + public function test_get_imported_posts_reads_all_chunks() { + global $wpdb; + + $post_id = self::factory()->post->create(); + + for ( $i = 1; $i <= 101; $i++ ) { + $wpdb->insert( + $wpdb->postmeta, + array( + 'post_id' => $post_id, + 'meta_key' => 'blogger_7_permalink', + 'meta_value' => "/post-{$i}.html", + ) + ); + } + + $imported = ( new WP_Importer() )->get_imported_posts( 'blogger', 7 ); + + $this->assertCount( 101, $imported, 'Permalinks after the first chunk should be included.' ); + $this->assertSame( $post_id, $imported['/post-101.html'] ); + } + + /** + * Tests that no imported posts gives an empty array. + * + * @ticket 65819 + * + * @covers WP_Importer::get_imported_posts + */ + public function test_get_imported_posts_returns_empty_array_without_imports() { + $this->assertSame( array(), ( new WP_Importer() )->get_imported_posts( 'blogger', 7 ) ); + } + + /** + * Tests the number of imported permalinks for the given importer and blog. + * + * @ticket 65819 + * + * @covers WP_Importer::count_imported_posts + */ + public function test_count_imported_posts() { + $post_id = self::factory()->post->create(); + + add_post_meta( $post_id, 'blogger_7_permalink', '/first.html' ); + add_post_meta( $post_id, 'blogger_7_permalink', '/second.html' ); + add_post_meta( $post_id, 'blogger_8_permalink', '/other-blog.html' ); + + $importer = new WP_Importer(); + + $this->assertSame( 2, $importer->count_imported_posts( 'blogger', 7 ), 'Permalinks for the blog should be counted.' ); + $this->assertSame( 0, $importer->count_imported_posts( 'blogger', 9 ), 'A blog without imports should have none.' ); + } + + /** + * Tests that imported comments are mapped from their source comment ID for the given blog. + * + * @ticket 65819 + * + * @covers WP_Importer::get_imported_comments + */ + public function test_get_imported_comments_maps_source_ids_for_blog() { + $post_id = self::factory()->post->create(); + + $first = self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_agent' => '7-15', + ) + ); + $second = self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_agent' => '7-16', + ) + ); + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_agent' => '8-17', + ) + ); + + $this->assertSame( + array( + 15 => $first, + 16 => $second, + ), + ( new WP_Importer() )->get_imported_comments( '7' ) + ); + } + + /** + * Tests that a numeric blog ID is returned as an integer. + * + * @ticket 65819 + * + * @covers WP_Importer::set_blog + * + * @group ms-excluded + */ + public function test_set_blog_returns_numeric_blog_id() { + $this->assertSame( 1, ( new WP_Importer() )->set_blog( '1' ) ); + } + + /** + * Tests that set_blog() switches to the blog on multisite, resolving it by ID or URL. + * + * @ticket 65819 + * + * @covers WP_Importer::set_blog + * + * @group ms-required + * + * @dataProvider data_set_blog_on_multisite + * + * @param string $blog Blog ID placeholder or URL passed to set_blog(). + */ + public function test_set_blog_switches_to_blog_on_multisite( $blog ) { + $blog_id = self::factory()->blog->create( + array( + 'domain' => 'importer.example.org', + 'path' => '/target/', + ) + ); + + $blog = str_replace( '%id%', $blog_id, $blog ); + + $result = ( new WP_Importer() )->set_blog( $blog ); + + $current = get_current_blog_id(); + restore_current_blog(); + + $this->assertSame( $blog_id, $result, 'The blog ID should be returned.' ); + $this->assertSame( $blog_id, $current, 'The blog should be switched to.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_set_blog_on_multisite() { + return array( + 'numeric ID' => array( '%id%' ), + 'URL with scheme' => array( 'https://importer.example.org/target/' ), + 'URL without scheme' => array( 'importer.example.org/target/' ), + ); + } + + /** + * Tests that set_user() sets the current user by ID or login. + * + * @ticket 65819 + * + * @covers WP_Importer::set_user + * + * @dataProvider data_set_user + * + * @param bool $by_login Whether to pass the user login instead of the ID. + */ + public function test_set_user( $by_login ) { + $user_id = self::factory()->user->create( array( 'user_login' => 'importer_user' ) ); + + $this->assertSame( $user_id, ( new WP_Importer() )->set_user( $by_login ? 'importer_user' : (string) $user_id ) ); + $this->assertSame( $user_id, get_current_user_id(), 'The user should be set as the current user.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_set_user() { + return array( + 'user ID' => array( false ), + 'user login' => array( true ), + ); + } + + /** + * Tests that cmpr_strlen() sorts the longest string first. + * + * @ticket 65819 + * + * @covers WP_Importer::cmpr_strlen + */ + public function test_cmpr_strlen_sorts_longest_first() { + $importer = new WP_Importer(); + $strings = array( 'bb', 'a', 'dddd', 'ccc' ); + + usort( $strings, array( $importer, 'cmpr_strlen' ) ); + + $this->assertSame( array( 'dddd', 'ccc', 'bb', 'a' ), $strings, 'Strings should be sorted by length, longest first.' ); + $this->assertSame( 0, $importer->cmpr_strlen( 'ab', 'cd' ), 'Strings of the same length should compare equal.' ); + $this->assertSame( 2, $importer->cmpr_strlen( 'a', 'abc' ), 'A shorter first string should sort after the longer one.' ); + } + + /** + * Tests the request made by get_page(). + * + * @ticket 65819 + * + * @covers WP_Importer::get_page + * @covers WP_Importer::bump_request_timeout + * + * @dataProvider data_get_page_requests + * + * @param string $username Username. + * @param string $password Password. + * @param bool $head Whether to make a HEAD request. + * @param string $method Expected request method. + * @param string|null $authorization Expected Authorization header, or null for none. + */ + public function test_get_page_request( $username, $password, $head, $method, $authorization ) { + $requests = array(); + + add_filter( + 'pre_http_request', + static function ( $response, $args, $url ) use ( &$requests ) { + $requests[] = array( $url, $args ); + + return array( + 'headers' => array(), + 'body' => 'page body', + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'cookies' => array(), + ); + }, + 10, + 3 + ); + + $response = ( new WP_Importer() )->get_page( 'https://example.org/feed', $username, $password, $head ); + + $this->assertSame( 'page body', wp_remote_retrieve_body( $response ), 'The response should be returned.' ); + $this->assertCount( 1, $requests, 'One request should be made.' ); + + list( $url, $args ) = $requests[0]; + + $this->assertSame( 'https://example.org/feed', $url, 'The URL did not match.' ); + $this->assertSame( $method, $args['method'], 'The request method did not match.' ); + $this->assertSame( 60, $args['timeout'], 'The request timeout should be bumped.' ); + $this->assertTrue( $args['reject_unsafe_urls'], 'The request should be a safe request.' ); + + if ( null === $authorization ) { + $this->assertArrayNotHasKey( 'Authorization', $args['headers'], 'No Authorization header should be sent.' ); + } else { + $this->assertSame( $authorization, $args['headers']['Authorization'], 'The Authorization header did not match.' ); + } + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_get_page_requests() { + return array( + 'GET without credentials' => array( '', '', false, 'GET', null ), + 'HEAD request' => array( '', '', true, 'HEAD', null ), + 'truthy head is not HEAD' => array( '', '', 1, 'GET', null ), + 'with credentials' => array( 'admin', 'secret', false, 'GET', 'Basic YWRtaW46c2VjcmV0' ), + 'username only' => array( 'admin', '', false, 'GET', null ), + 'password only' => array( '', 'secret', false, 'GET', null ), + ); + } + + /** + * Tests that bump_request_timeout() always returns 60 seconds. + * + * @ticket 65819 + * + * @covers WP_Importer::bump_request_timeout + */ + public function test_bump_request_timeout() { + $this->assertSame( 60, ( new WP_Importer() )->bump_request_timeout( 5 ) ); + } + + /** + * Tests that the user is never over quota on a single site. + * + * @ticket 65819 + * + * @covers WP_Importer::is_user_over_quota + * + * @group ms-excluded + */ + public function test_is_user_over_quota_on_single_site() { + $this->assertFalse( ( new WP_Importer() )->is_user_over_quota() ); + } + + /** + * Tests the quota check on multisite. + * + * @ticket 65819 + * + * @covers WP_Importer::is_user_over_quota + * + * @group ms-required + * + * @dataProvider data_quota_usage + * + * @param int $space_used Space used, in megabytes. + * @param bool $expected Whether the user should be over quota. + * @param string $output Expected output. + */ + public function test_is_user_over_quota_on_multisite( $space_used, $expected, $output ) { + require_once ABSPATH . 'wp-admin/includes/ms.php'; + + update_site_option( 'upload_space_check_disabled', 0 ); + add_filter( + 'get_space_allowed', + static function () { + return 10; + } + ); + add_filter( + 'pre_get_space_used', + static function () use ( $space_used ) { + return $space_used; + } + ); + + $this->expectOutputString( $output ); + $this->assertSame( $expected, ( new WP_Importer() )->is_user_over_quota() ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_quota_usage() { + return array( + 'under quota' => array( 5, false, '' ), + 'over quota' => array( 20, true, 'Sorry, you have used your space allocation of 10 MB. Please delete some files to upload more files.' ), + ); + } + + /** + * Tests that runs of whitespace are replaced with a single space. + * + * @ticket 65819 + * + * @covers WP_Importer::min_whitespace + * + * @dataProvider data_min_whitespace + * + * @param string $text Text. + * @param string $expected Expected text. + */ + public function test_min_whitespace( $text, $expected ) { + $this->assertSame( $expected, ( new WP_Importer() )->min_whitespace( $text ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_min_whitespace() { + return array( + 'mixed whitespace' => array( "one \r\n\ttwo three", 'one two three' ), + 'single spaces' => array( 'one two', 'one two' ), + 'leading and trailing' => array( "\n one \t", ' one ' ), + 'no whitespace' => array( 'one', 'one' ), + 'empty string' => array( '', '' ), + 'zero string' => array( '0', '0' ), + ); + } + + /** + * Tests that stop_the_insanity() resets the query log and the action counts. + * + * @ticket 65819 + * + * @covers WP_Importer::stop_the_insanity + */ + public function test_stop_the_insanity_resets_queries_and_actions() { + global $wpdb, $wp_actions; + + $orig_queries = $wpdb->queries; + $orig_wp_actions = $wp_actions; + + try { + $wpdb->queries = array( array( 'SELECT 1', 0.1, 'caller' ) ); + do_action( 'test_stop_the_insanity' ); + + ( new WP_Importer() )->stop_the_insanity(); + + $this->assertSame( array(), $wpdb->queries, 'The query log should be emptied.' ); + $this->assertSame( array(), $wp_actions, 'The action counts should be emptied.' ); + } finally { + $wpdb->queries = $orig_queries; + $wp_actions = $orig_wp_actions; + } + } + + /** + * Tests the command line arguments returned by get_cli_args(). + * + * @ticket 65819 + * + * @covers ::get_cli_args + * + * @dataProvider data_get_cli_args + * + * @param array $argv Command line arguments, including the script name. + * @param string $param Parameter name. + * @param string|true|null $expected Expected value. + */ + public function test_get_cli_args( $argv, $param, $expected ) { + $_SERVER['argv'] = $argv; + + $this->assertSame( $expected, get_cli_args( $param ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_get_cli_args() { + return array( + 'long option with a value' => array( array( 'import.php', '--blog=7' ), 'blog', '7' ), + 'long option without a value' => array( array( 'import.php', '--verbose' ), 'verbose', true ), + 'value after a long option' => array( array( 'import.php', '--user', 'admin' ), 'user', 'admin' ), + 'short options' => array( array( 'import.php', '-vq' ), 'q', true ), + 'value after short options' => array( array( 'import.php', '-vq', 'quiet' ), 'q', 'quiet' ), + 'later option overrides' => array( array( 'import.php', '--blog=1', '--blog=2' ), 'blog', '2' ), + 'script name is ignored' => array( array( '--blog=7' ), 'blog', null ), + 'value without an option' => array( array( 'import.php', 'orphan' ), 'orphan', null ), + 'missing optional parameter' => array( array( 'import.php', '--blog=7' ), 'user', null ), + 'empty value' => array( array( 'import.php', '--blog=' ), 'blog', '' ), + 'zero value' => array( array( 'import.php', '--blog=0' ), 'blog', '0' ), + ); + } + + /** + * Tests that get_cli_args() returns null when `$_SERVER['argv']` is not an array. + * + * @ticket 65819 + * + * @covers ::get_cli_args + */ + public function test_get_cli_args_without_argv_array() { + $_SERVER['argv'] = 'not an array'; + + $this->assertNull( get_cli_args( 'blog' ) ); + } +} diff --git a/tests/phpunit/tests/admin/wpListTable.php b/tests/phpunit/tests/admin/wpListTable.php index b3de66659b308..909b232a2020c 100644 --- a/tests/phpunit/tests/admin/wpListTable.php +++ b/tests/phpunit/tests/admin/wpListTable.php @@ -17,14 +17,22 @@ class Tests_Admin_WpListTable extends WP_UnitTestCase { /** * Original value of $GLOBALS['hook_suffix']. * - * @var string + * @var string|null */ private static $original_hook_suffix; + /** + * Whether $GLOBALS['hook_suffix'] existed before the test class ran. + * + * @var bool + */ + private static $hook_suffix_was_set; + public static function set_up_before_class() { parent::set_up_before_class(); - static::$original_hook_suffix = $GLOBALS['hook_suffix']; + static::$hook_suffix_was_set = array_key_exists( 'hook_suffix', $GLOBALS ); + static::$original_hook_suffix = $GLOBALS['hook_suffix'] ?? null; require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } @@ -37,8 +45,12 @@ public function set_up() { } public function clean_up_global_scope() { - global $hook_suffix; - $hook_suffix = static::$original_hook_suffix; + if ( static::$hook_suffix_was_set ) { + $GLOBALS['hook_suffix'] = static::$original_hook_suffix; + } else { + unset( $GLOBALS['hook_suffix'] ); + } + parent::clean_up_global_scope(); } @@ -61,13 +73,10 @@ public function test_should_only_add_primary_column_when_needed( $list_class, $h $hook = new MockAction(); add_filter( 'list_table_primary_column', array( $hook, 'filter' ) ); - /* - * Set a dummy value for the current screen in the admin to prevent - * `_get_list_table()` throwing. - */ - $GLOBALS['hook_suffix'] = 'my-hook'; - - $list_table = _get_list_table( $list_class ); + $list_table = _get_list_table( + $list_class, + array( 'screen' => 'wp-list-table-test-' . sanitize_key( $list_class ) ) + ); $column_headers = new ReflectionProperty( $list_table, '_column_headers' ); if ( PHP_VERSION_ID < 80100 ) { @@ -89,7 +98,7 @@ public function test_should_only_add_primary_column_when_needed( $list_class, $h * * @return array */ - public function data_should_only_add_primary_column_when_needed() { + public static function data_should_only_add_primary_column_when_needed() { /* * `WP_Post_Comments_List_Table` overrides `get_column_info()` rather than * use the default `WP_List_Table::get_column_info()`. Therefore it is @@ -97,7 +106,7 @@ public function data_should_only_add_primary_column_when_needed() { */ $list_primary_columns = array( 'WP_Application_Passwords_List_Table' => 'name', - 'WP_Comments_List_Table' => 'author', + 'WP_Comments_List_Table' => 'comment', 'WP_Links_List_Table' => 'name', 'WP_Media_List_Table' => 'title', 'WP_MS_Sites_List_Table' => 'blogname', @@ -178,7 +187,7 @@ public function test_get_views_links( $link_data, $expected ) { * * @return array */ - public function data_get_views_links() { + public static function data_get_views_links() { return array( 'one "current" link' => array( 'link_data' => array( @@ -292,7 +301,7 @@ public function test_get_views_links_doing_it_wrong( $link_data ) { * * @return array */ - public function data_get_views_links_doing_it_wrong() { + public static function data_get_views_links_doing_it_wrong() { return array( 'non-array $link_data' => array( 'link_data' => 'https://example.org, All, class="current" aria-current="page"', @@ -500,7 +509,7 @@ public function test_should_throw_deprecation_when_unset_of_dynamic_property() { * * @return array */ - public function data_compat_fields() { + public static function data_compat_fields() { return array( '_args' => array( 'property_name' => '_args', @@ -592,4 +601,59 @@ public function test_search_box_works_with_orderby_string() { $this->assertStringContainsString( $expected_html, $actual ); } + + /** + * Tests that `WP_List_Table::_js_vars()` prints its data through + * `wp_print_inline_script_tag()`, so attributes such as a per-request nonce + * can be attached via the `wp_inline_script_attributes` filter. + * + * @ticket 59446 + * + * @covers WP_List_Table::_js_vars + */ + public function test_js_vars_prints_inline_script_tag_with_filterable_attributes() { + add_filter( + 'wp_inline_script_attributes', + static function ( array $attributes ): array { + $attributes['nonce'] = 'test-list-table-nonce'; + return $attributes; + } + ); + + $actual = get_echo( array( $this->list_table, '_js_vars' ) ); + + $processor = new WP_HTML_Tag_Processor( $actual ); + $this->assertTrue( $processor->next_tag( 'SCRIPT' ), 'The expected SCRIPT tag was not printed.' ); + $this->assertSame( 'test-list-table-nonce', $processor->get_attribute( 'nonce' ), 'The nonce attribute added via wp_inline_script_attributes was not printed.' ); + $this->assertStringContainsString( 'list_args =', $processor->get_modifiable_text(), 'The expected JavaScript variable was not printed.' ); + } + + /** + * Tests that `WP_Themes_List_Table::_js_vars()` prints its data through + * `wp_print_inline_script_tag()`, so attributes such as a per-request nonce + * can be attached via the `wp_inline_script_attributes` filter. + * + * @ticket 59446 + * + * @covers WP_Themes_List_Table::_js_vars + */ + public function test_themes_list_table_js_vars_prints_inline_script_tag_with_filterable_attributes() { + $themes_list_table = _get_list_table( WP_Themes_List_Table::class, array( 'screen' => 'themes' ) ); + $this->assertInstanceOf( WP_Themes_List_Table::class, $themes_list_table, 'Expected list table class.' ); + + add_filter( + 'wp_inline_script_attributes', + static function ( array $attributes ): array { + $attributes['nonce'] = 'test-themes-list-table-nonce'; + return $attributes; + } + ); + + $actual = get_echo( array( $themes_list_table, '_js_vars' ) ); + + $processor = new WP_HTML_Tag_Processor( $actual ); + $this->assertTrue( $processor->next_tag( 'SCRIPT' ), 'The expected SCRIPT tag was not printed.' ); + $this->assertSame( 'test-themes-list-table-nonce', $processor->get_attribute( 'nonce' ), 'The nonce attribute added via wp_inline_script_attributes was not printed.' ); + $this->assertStringContainsString( 'theme_list_args =', $processor->get_modifiable_text(), 'The expected JavaScript variable was not printed.' ); + } } diff --git a/tests/phpunit/tests/admin/wpMediaListTable.php b/tests/phpunit/tests/admin/wpMediaListTable.php index 31d2bfa7cbb9d..05dea63c8fbc2 100644 --- a/tests/phpunit/tests/admin/wpMediaListTable.php +++ b/tests/phpunit/tests/admin/wpMediaListTable.php @@ -206,7 +206,7 @@ public function test_get_row_actions_should_include_action( $action, $role, $tra * * @return array[] */ - public function data_get_row_actions_should_include_action() { + public static function data_get_row_actions_should_include_action() { return array( '"edit" while not on "trash"' => array( 'action' => 'edit', @@ -291,7 +291,7 @@ public function test_get_row_actions_should_not_include_action( $action, $role, * * @return array[] */ - public function data_get_row_actions_should_not_include_action() { + public static function data_get_row_actions_should_not_include_action() { return array( '"edit" while on "trash"' => array( 'action' => 'edit', diff --git a/tests/phpunit/tests/admin/wpMenuOutput.php b/tests/phpunit/tests/admin/wpMenuOutput.php new file mode 100644 index 0000000000000..00734e775b731 --- /dev/null +++ b/tests/phpunit/tests/admin/wpMenuOutput.php @@ -0,0 +1,177 @@ +user->create( array( 'role' => 'administrator' ) ) ); + } + + /** + * Dashicons back-compat: the Dashicons of the core menu items are kept in the menu data and rendered as SVG icons. + * + * @ticket 65089 + * + * @covers ::_wp_menu_output + * + * @dataProvider data_replaced_dashicons + * + * @param string $dashicon Dashicon class name. + */ + public function test_core_dashicon_is_rendered_as_svg_icon( $dashicon ): void { + // 0 = menu_title, 1 = capability, 2 = menu_slug, 3 = page_title, 4 = classes, 5 = hookname, 6 = icon_url. + $menu = array( + array( 'Item', 'read', 'my-plugin', '', 'menu-top', 'toplevel_page_my-plugin', $dashicon ), + ); + $html = get_echo( '_wp_menu_output', array( $menu, array() ) ); + + $this->assertMatchesRegularExpression( "##s", $html ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_replaced_dashicons(): array { + return array( + 'dashboard' => array( 'dashicons-dashboard' ), + 'posts' => array( 'dashicons-admin-post' ), + 'media' => array( 'dashicons-admin-media' ), + 'links' => array( 'dashicons-admin-links' ), + 'pages' => array( 'dashicons-admin-page' ), + 'comments' => array( 'dashicons-admin-comments' ), + 'appearance' => array( 'dashicons-admin-appearance' ), + 'plugins' => array( 'dashicons-admin-plugins' ), + 'users' => array( 'dashicons-admin-users' ), + 'tools' => array( 'dashicons-admin-tools' ), + 'settings' => array( 'dashicons-admin-settings' ), + 'sites' => array( 'dashicons-admin-multisite' ), + ); + } + + /** + * Dashicons back-compat: any other icon value is rendered as before. + * + * @ticket 65089 + * + * @covers ::_wp_menu_output + * + * @dataProvider data_unchanged_icons + * + * @param string $icon The icon of the menu item. + * @param string $expected The expected HTML of the icon element. + */ + public function test_other_icon_is_rendered_as_before( $icon, $expected ): void { + // 0 = menu_title, 1 = capability, 2 = menu_slug, 3 = page_title, 4 = classes, 5 = hookname, 6 = icon_url. + $menu = array( + array( 'Item', 'read', 'my-plugin', '', 'menu-top', 'toplevel_page_my-plugin', $icon ), + ); + $html = get_echo( '_wp_menu_output', array( $menu, array() ) ); + + $this->assertStringContainsString( $expected, $html ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_unchanged_icons(): array { + return array( + 'other Dashicon' => array( + 'dashicons-heart', + "", + ), + 'image URL' => array( + 'https://example.org/icon.png', + "", + ), + 'none' => array( + 'none', + "", + ), + 'div' => array( + 'div', + "", + ), + 'base64 SVG' => array( + 'data:image/svg+xml;base64,PHN2Zy8+', + "", + ), + ); + } + + /** + * @ticket 65089 + * + * @covers ::_wp_menu_output + */ + public function test_icon_name_is_rendered_as_svg_icon(): void { + // 0 = menu_title, 1 = capability, 2 = menu_slug, 3 = page_title, 4 = classes, 5 = hookname, 6 = icon_url. + $menu = array( + array( 'Item', 'read', 'my-plugin', '', 'menu-top', 'toplevel_page_my-plugin', 'core-admin/pin' ), + ); + $html = get_echo( '_wp_menu_output', array( $menu, array() ) ); + + $this->assertStringContainsString( "', $html ); + } + + /** + * @ticket 65089 + * + * @covers ::_wp_menu_output + * + * @expectedIncorrectUsage _wp_menu_output + */ + public function test_unregistered_icon_name_leaves_icon_element_empty(): void { + // 0 = menu_title, 1 = capability, 2 = menu_slug, 3 = page_title, 4 = classes, 5 = hookname, 6 = icon_url. + $menu = array( + array( 'Item', 'read', 'my-plugin', '', 'menu-top', 'toplevel_page_my-plugin', 'my-plugin/not-registered' ), + ); + $html = get_echo( '_wp_menu_output', array( $menu, array() ) ); + + $this->assertStringContainsString( "", $html ); + } +} diff --git a/tests/phpunit/tests/admin/wpPluginInstallListTableGetDependenciesNotice.php b/tests/phpunit/tests/admin/wpPluginInstallListTableGetDependenciesNotice.php new file mode 100644 index 0000000000000..59f09c991a7f4 --- /dev/null +++ b/tests/phpunit/tests/admin/wpPluginInstallListTableGetDependenciesNotice.php @@ -0,0 +1,368 @@ +

    Additional plugins are required

    '; + + /** + * The list table. + * + * @var WP_Plugin_Install_List_Table + */ + private $table; + + /** + * The original values of the globals and properties the tests change. + * + * @var array + */ + private $original = array(); + + /** + * Slugs requested from the plugins API. + * + * @var string[] + */ + private $api_requests = array(); + + /** + * Responses of the mocked plugins API, keyed by slug. + * + * @var array + */ + private $api_responses = array(); + + /** + * Loads plugins_api(), which the Add Plugins screen loads. + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + + require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; + } + + /** + * Sets up the list table, the Add Plugins screen and the plugins API mock. + */ + public function set_up() { + parent::set_up(); + + $this->original = array( + 'pagenow' => $GLOBALS['pagenow'] ?? null, + 'current_screen' => $GLOBALS['current_screen'] ?? null, + 'dependency_api_data' => $this->get_dependency_api_data(), + ); + + $this->table = _get_list_table( 'WP_Plugin_Install_List_Table', array( 'screen' => 'plugin-install' ) ); + + $GLOBALS['pagenow'] = 'plugin-install.php'; + set_current_screen( 'plugin-install' ); + $this->set_dependency_api_data( array() ); + + add_filter( 'plugins_api', array( $this, 'mock_plugins_api' ), 10, 3 ); + } + + /** + * Restores the globals and properties. + */ + public function tear_down() { + $GLOBALS['pagenow'] = $this->original['pagenow']; + $GLOBALS['current_screen'] = $this->original['current_screen']; + $this->set_dependency_api_data( $this->original['dependency_api_data'] ); + + parent::tear_down(); + } + + /** + * Tests that no notice is returned for a plugin without dependencies. + * + * @ticket 65819 + * + * @dataProvider data_no_dependencies + * + * @param array $plugin_data The plugin data. + */ + public function test_should_return_empty_string_without_dependencies( $plugin_data ) { + $this->assertSame( '', $this->get_dependencies_notice( $plugin_data ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_no_dependencies() { + return array( + 'no requires_plugins key' => array( array( 'slug' => 'my-plugin' ) ), + 'empty array' => array( array( 'requires_plugins' => array() ) ), + 'empty string' => array( array( 'requires_plugins' => '' ) ), + ); + } + + /** + * Tests that a dependency is named from the stored dependency data. + * + * @ticket 65819 + */ + public function test_should_name_dependency_from_dependency_data() { + $this->set_dependency_api_data( + array( + 'dep-one' => array( + 'name' => 'Dep & "One"', + 'slug' => 'dep-one', + 'version' => '1.0', + ), + ) + ); + + $this->assertSame( + self::NOTICE_START . $this->get_named_dependency( 'Dep <b>&</b> "One"', 'dep-one' ) . '
    ', + $this->get_dependencies_notice( array( 'requires_plugins' => array( 'dep-one' ) ) ), + 'The dependency should be named from the stored data.' + ); + $this->assertSame( array(), $this->api_requests, 'The plugins API should not be requested.' ); + } + + /** + * Tests that the details link uses the slug of the stored dependency data. + * + * @ticket 65819 + */ + public function test_should_use_slug_from_dependency_data() { + $this->set_dependency_api_data( + array( + 'dep-one' => array( + 'name' => 'Dep One', + 'slug' => 'dep-one-renamed', + 'version' => '1.0', + ), + ) + ); + + $this->assertSame( + self::NOTICE_START . $this->get_named_dependency( 'Dep One', 'dep-one-renamed' ) . '

    ', + $this->get_dependencies_notice( array( 'requires_plugins' => array( 'dep-one' ) ) ) + ); + } + + /** + * Tests that incomplete dependency data falls back to the plugins API. + * + * @ticket 65819 + * + * @dataProvider data_incomplete_dependency_data + * + * @param array|false $dependency_data The stored dependency data, or false for none. + */ + public function test_should_fall_back_to_plugins_api_for_incomplete_dependency_data( $dependency_data ) { + if ( false !== $dependency_data ) { + $this->set_dependency_api_data( array( 'dep-one' => $dependency_data ) ); + } + + $this->api_responses['dep-one'] = (object) array( + 'name' => 'Dep One From API', + 'slug' => 'dep-one', + ); + + $this->assertSame( + self::NOTICE_START . $this->get_named_dependency( 'Dep One From API', 'dep-one' ) . '', + $this->get_dependencies_notice( array( 'requires_plugins' => array( 'dep-one' ) ) ), + 'The dependency should be named from the plugins API.' + ); + $this->assertSame( array( 'dep-one' ), $this->api_requests, 'The plugins API should be requested once.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_incomplete_dependency_data() { + $complete = array( + 'name' => 'Dep One', + 'slug' => 'dep-one', + 'version' => '1.0', + ); + + return array( + 'no dependency data' => array( false ), + 'empty name' => array( array_merge( $complete, array( 'name' => '' ) ) ), + 'empty slug' => array( array_merge( $complete, array( 'slug' => '' ) ) ), + 'empty version' => array( array_merge( $complete, array( 'version' => '' ) ) ), + 'no version' => array( array_diff_key( $complete, array( 'version' => true ) ) ), + ); + } + + /** + * Tests that the details link uses the slug returned by the plugins API. + * + * @ticket 65819 + */ + public function test_should_use_slug_from_plugins_api() { + $this->api_responses['dep-one'] = (object) array( + 'name' => 'Dep One', + 'slug' => 'renamed-dep', + ); + + $this->assertSame( + self::NOTICE_START . $this->get_named_dependency( 'Dep <i>One</i>', 'renamed-dep' ) . '', + $this->get_dependencies_notice( array( 'requires_plugins' => array( 'dep-one' ) ) ) + ); + } + + /** + * Tests that a dependency without a name is listed by its slug. + * + * @ticket 65819 + * + * @dataProvider data_unnamed_api_responses + * + * @param string $response The plugins API response type. + */ + public function test_should_list_dependency_by_slug_without_name( $response ) { + $this->api_responses['dep'] = 'error' === $response + ? new WP_Error( 'plugins_api_failed', 'An error.' ) + : (object) array( + 'name' => '', + 'slug' => 'dep-one', + ); + + $this->assertSame( + self::NOTICE_START . '
    dep<one>
    ', + $this->get_dependencies_notice( array( 'requires_plugins' => array( 'dep' ) ) ) + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_unnamed_api_responses() { + return array( + 'API error' => array( 'error' ), + 'API data, no name' => array( 'no name' ), + ); + } + + /** + * Tests that every dependency is listed, in order. + * + * @ticket 65819 + */ + public function test_should_list_all_dependencies_in_order() { + $this->set_dependency_api_data( + array( + 'dep-one' => array( + 'name' => 'Dep One', + 'slug' => 'dep-one', + 'version' => '1.0', + ), + ) + ); + + $this->api_responses['dep-two'] = (object) array( + 'name' => 'Dep Two', + 'slug' => 'dep-two', + ); + $this->api_responses['dep-three'] = new WP_Error( 'plugins_api_failed', 'An error.' ); + + $this->assertSame( + self::NOTICE_START + . $this->get_named_dependency( 'Dep One', 'dep-one' ) + . $this->get_named_dependency( 'Dep Two', 'dep-two' ) + . '
    dep-three
    ' + . '', + $this->get_dependencies_notice( array( 'requires_plugins' => array( 'dep-one', 'dep-two', 'dep-three' ) ) ), + 'Each dependency should be listed in order.' + ); + $this->assertSame( array( 'dep-two', 'dep-three' ), $this->api_requests, 'Only dependencies without stored data should be requested.' ); + } + + /** + * Returns the expected markup of a named dependency. + * + * @param string $escaped_name The escaped plugin name. + * @param string $slug The plugin slug. + * @return string The markup. + */ + private function get_named_dependency( $escaped_name, $slug ) { + $url = 'http://' . WP_TESTS_DOMAIN . '/wp-admin/' . ( is_multisite() ? 'network/' : '' ) + . 'plugin-install.php?tab=plugin-information&plugin=' . $slug . '&TB_iframe=true&width=600&height=550'; + + return '
    ' . $escaped_name . ' ' + . 'More Details' + . '
    '; + } + + /** + * Calls the protected get_dependencies_notice() method. + * + * @param array $plugin_data The plugin data. + * @return string The notice. + */ + private function get_dependencies_notice( $plugin_data ) { + $method = new ReflectionMethod( $this->table, 'get_dependencies_notice' ); + if ( PHP_VERSION_ID < 80100 ) { + $method->setAccessible( true ); + } + + return $method->invoke( $this->table, $plugin_data ); + } + + /** + * Gets the stored dependency data of WP_Plugin_Dependencies. + * + * @return array|null The dependency data. + */ + private function get_dependency_api_data() { + $property = new ReflectionProperty( 'WP_Plugin_Dependencies', 'dependency_api_data' ); + if ( PHP_VERSION_ID < 80100 ) { + $property->setAccessible( true ); + } + + return $property->getValue(); + } + + /** + * Sets the stored dependency data of WP_Plugin_Dependencies. + * + * @param array|null $value The dependency data. + */ + private function set_dependency_api_data( $value ) { + $property = new ReflectionProperty( 'WP_Plugin_Dependencies', 'dependency_api_data' ); + if ( PHP_VERSION_ID < 80100 ) { + $property->setAccessible( true ); + } + + $property->setValue( null, $value ); + } + + /** + * Returns the mocked plugins API response for the requested slug. + * + * @param false|object|array $result The result object or array. + * @param string $action The type of information being requested. + * @param object $args Plugin API arguments. + * @return object|WP_Error The mocked response. + */ + public function mock_plugins_api( $result, $action, $args ) { + $this->api_requests[] = $args->slug; + + return $this->api_responses[ $args->slug ] ?? new WP_Error( 'plugins_api_failed', 'No mocked response.' ); + } +} diff --git a/tests/phpunit/tests/admin/wpPluginsListTable.php b/tests/phpunit/tests/admin/wpPluginsListTable.php index daa54750fdf9e..b6b8fd3d6002d 100644 --- a/tests/phpunit/tests/admin/wpPluginsListTable.php +++ b/tests/phpunit/tests/admin/wpPluginsListTable.php @@ -286,12 +286,85 @@ public function test_single_row_should_not_add_the_autoupdates_column_for_mustus $this->assertStringNotContainsString( 'column-auto-updates', $actual, 'The auto-updates column was output.' ); } + /** + * Tests that WP_Plugins_List_Table::single_row() determines the auto-update + * message once, rather than for each plugin with an update. + * + * The 'ms-excluded' group is added as the auto-updates column is only shown in the network admin. + * + * @ticket 27814 + * + * @group ms-excluded + * + * @covers WP_Plugins_List_Table::single_row + */ + public function test_single_row_should_determine_the_auto_update_message_once() { + add_filter( 'plugins_auto_update_enabled', '__return_true' ); + add_filter( 'request_filesystem_credentials', '__return_true' ); + + $vcs_checks = 0; + add_filter( + 'automatic_updates_is_vcs_checkout', + static function () use ( &$vcs_checks ) { + ++$vcs_checks; + return true; + } + ); + + wp_set_current_user( self::$admin_id ); + + $list_table_mock = $this->getMockBuilder( 'WP_Plugins_List_Table' ) + ->setMethods( array( 'get_column_info' ) ) + ->getMock(); + + $list_table_mock->method( 'get_column_info' )->willReturn( + array( + array( + 'name' => 'Plugin', + 'auto-updates' => 'Auto-updates', + ), + array(), + array(), + 'name', + ) + ); + + $output = ''; + foreach ( array( 'plugin-one', 'plugin-two' ) as $slug ) { + ob_start(); + $list_table_mock->single_row( + array( + "$slug/$slug.php", + array( + 'Name' => $slug, + 'slug' => $slug, + 'Description' => 'A plugin with an update available.', + 'Author' => 'A plugin author', + 'Version' => '1.0.0', + 'Author URI' => 'http://example.org', + 'Text Domain' => $slug, + 'update' => true, + 'update-supported' => true, + ), + ) + ); + $output .= ob_get_clean(); + } + + $this->assertSame( 1, $vcs_checks, 'Version control was not checked exactly once.' ); + $this->assertSame( + 2, + substr_count( $output, 'appears to be under version control' ), + 'The auto-update message was not output for both plugins.' + ); + } + /** * Data provider. * * @return array[] */ - public function data_status_mustuse_and_dropins() { + public static function data_status_mustuse_and_dropins() { return array( 'Must-Use' => array( 'mustuse' ), 'Drop-ins' => array( 'dropins' ), diff --git a/tests/phpunit/tests/admin/wpPluginsListTableGetBulkActions.php b/tests/phpunit/tests/admin/wpPluginsListTableGetBulkActions.php new file mode 100644 index 0000000000000..30bdb4a38f3c6 --- /dev/null +++ b/tests/phpunit/tests/admin/wpPluginsListTableGetBulkActions.php @@ -0,0 +1,301 @@ +user->create( array( 'role' => 'administrator' ) ); + + if ( is_multisite() ) { + grant_super_admin( self::$admin_id ); + } + + self::$activator_id = $factory->user->create( array( 'role' => 'subscriber' ) ); + get_userdata( self::$activator_id )->add_cap( 'activate_plugins' ); + } + + /** + * Revokes the super admin grant before the fixture users are deleted. + */ + public static function wpTearDownAfterClass() { + if ( is_multisite() ) { + revoke_super_admin( self::$admin_id ); + } + } + + /** + * Saves the globals the list table sets and enables plugin auto-updates. + */ + public function set_up() { + parent::set_up(); + + // Auto-updates are disabled in the test environment. + add_filter( 'plugins_auto_update_enabled', '__return_true' ); + + $this->original_globals = array( + 'status' => $GLOBALS['status'] ?? null, + 'page' => $GLOBALS['page'] ?? null, + ); + } + + /** + * Restores the globals the list table sets. + */ + public function tear_down() { + foreach ( $this->original_globals as $name => $value ) { + $GLOBALS[ $name ] = $value; + } + + unset( $_REQUEST['plugin_status'] ); + + parent::tear_down(); + } + + /** + * Tests the bulk actions for each plugin status on the Plugins screen of a single site. + * + * @ticket 65819 + * + * @group ms-excluded + * + * @dataProvider data_statuses + * + * @param string $status The plugin status. + * @param string[] $expected The expected bulk action names. + */ + public function test_should_return_bulk_actions_for_status( $status, $expected ) { + wp_set_current_user( self::$admin_id ); + + $this->assertSame( $expected, array_keys( $this->get_bulk_actions( 'plugins', $status ) ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_statuses() { + $all = array( + 'activate-selected', + 'deactivate-selected', + 'update-selected', + 'delete-selected', + 'enable-auto-update-selected', + 'disable-auto-update-selected', + ); + + return array( + 'all' => array( 'all', $all ), + 'upgrade' => array( 'upgrade', $all ), + 'active' => array( + 'active', + array( + 'deactivate-selected', + 'update-selected', + 'enable-auto-update-selected', + 'disable-auto-update-selected', + ), + ), + 'inactive' => array( + 'inactive', + array( + 'activate-selected', + 'update-selected', + 'delete-selected', + 'enable-auto-update-selected', + 'disable-auto-update-selected', + ), + ), + // The Recently Active view; get_bulk_actions() checks for 'recent', so Deactivate is offered. + 'recently_activated' => array( 'recently_activated', $all ), + 'recent' => array( + 'recent', + array( + 'activate-selected', + 'update-selected', + 'delete-selected', + 'enable-auto-update-selected', + 'disable-auto-update-selected', + ), + ), + 'auto-update-enabled' => array( + 'auto-update-enabled', + array( + 'activate-selected', + 'deactivate-selected', + 'update-selected', + 'delete-selected', + 'disable-auto-update-selected', + ), + ), + 'auto-update-disabled' => array( + 'auto-update-disabled', + array( + 'activate-selected', + 'deactivate-selected', + 'update-selected', + 'delete-selected', + 'enable-auto-update-selected', + ), + ), + ); + } + + /** + * Tests the labels of the bulk actions on the Plugins screen of a single site. + * + * @ticket 65819 + * + * @group ms-excluded + */ + public function test_should_return_bulk_action_labels() { + wp_set_current_user( self::$admin_id ); + + $this->assertSame( + array( + 'activate-selected' => 'Activate', + 'deactivate-selected' => 'Deactivate', + 'update-selected' => 'Update', + 'delete-selected' => 'Delete', + 'enable-auto-update-selected' => 'Enable Auto-updates', + 'disable-auto-update-selected' => 'Disable Auto-updates', + ), + $this->get_bulk_actions( 'plugins', 'all' ) + ); + } + + /** + * Tests that the auto-update actions are left out when plugin auto-updates are disabled. + * + * @ticket 65819 + * + * @group ms-excluded + */ + public function test_should_not_return_auto_update_actions_when_auto_updates_are_disabled() { + wp_set_current_user( self::$admin_id ); + + add_filter( 'plugins_auto_update_enabled', '__return_false', 11 ); + + $this->assertSame( + array( + 'activate-selected', + 'deactivate-selected', + 'update-selected', + 'delete-selected', + ), + array_keys( $this->get_bulk_actions( 'plugins', 'all' ) ) + ); + } + + /** + * Tests that a user who can only activate plugins gets only the activation actions. + * + * @ticket 65819 + */ + public function test_should_return_only_activation_actions_for_user_who_cannot_update_or_delete() { + wp_set_current_user( self::$activator_id ); + + $this->assertSame( + array( + 'activate-selected' => 'Activate', + 'deactivate-selected' => 'Deactivate', + ), + $this->get_bulk_actions( 'plugins', 'all' ) + ); + } + + /** + * Tests that the Plugins screen of a site in a network only offers the activation actions. + * + * @ticket 65819 + * + * @group ms-required + */ + public function test_should_return_only_activation_actions_on_site_screen_in_network() { + wp_set_current_user( self::$admin_id ); + + $this->assertSame( + array( + 'activate-selected' => 'Activate', + 'deactivate-selected' => 'Deactivate', + ), + $this->get_bulk_actions( 'plugins', 'all' ) + ); + } + + /** + * Tests the bulk actions on the Plugins screen of the network admin. + * + * @ticket 65819 + * + * @group ms-required + */ + public function test_should_return_network_bulk_actions_on_network_screen() { + wp_set_current_user( self::$admin_id ); + + $this->assertSame( + array( + 'activate-selected' => 'Network Activate', + 'deactivate-selected' => 'Network Deactivate', + 'update-selected' => 'Update', + 'delete-selected' => 'Delete', + 'enable-auto-update-selected' => 'Enable Auto-updates', + 'disable-auto-update-selected' => 'Disable Auto-updates', + ), + $this->get_bulk_actions( 'plugins-network', 'all' ) + ); + } + + /** + * Returns the bulk actions of a Plugins list table. + * + * @param string $screen The screen ID. + * @param string $status The plugin status, as requested. + * @return string[] The bulk actions. + */ + private function get_bulk_actions( $screen, $status ) { + $_REQUEST['plugin_status'] = $status; + + $table = _get_list_table( 'WP_Plugins_List_Table', array( 'screen' => $screen ) ); + + $method = new ReflectionMethod( $table, 'get_bulk_actions' ); + if ( PHP_VERSION_ID < 80100 ) { + $method->setAccessible( true ); + } + + return $method->invoke( $table ); + } +} diff --git a/tests/phpunit/tests/admin/wpPluginsListTableNoItems.php b/tests/phpunit/tests/admin/wpPluginsListTableNoItems.php new file mode 100644 index 0000000000000..7135207da8c6f --- /dev/null +++ b/tests/phpunit/tests/admin/wpPluginsListTableNoItems.php @@ -0,0 +1,216 @@ +user->create( array( 'role' => 'administrator' ) ); + self::$subscriber_id = $factory->user->create( array( 'role' => 'subscriber' ) ); + } + + /** + * Saves the plugins global. + */ + public function set_up() { + parent::set_up(); + + $this->original_plugins = $GLOBALS['plugins'] ?? null; + $this->original_request_uri = $_SERVER['REQUEST_URI'] ?? null; + $GLOBALS['plugins'] = array( 'all' => array() ); + } + + /** + * Restores the plugins global and the search request. + */ + public function tear_down() { + $GLOBALS['plugins'] = $this->original_plugins; + + if ( null === $this->original_request_uri ) { + unset( $_SERVER['REQUEST_URI'] ); + } else { + $_SERVER['REQUEST_URI'] = $this->original_request_uri; + } + + unset( $_REQUEST['s'] ); + + parent::tear_down(); + } + + /** + * Tests the message when no plugins are installed. + * + * @ticket 65819 + */ + public function test_should_report_no_plugins_available() { + wp_set_current_user( self::$admin_id ); + + $this->assertSame( 'No plugins are currently available.', $this->get_no_items() ); + } + + /** + * Tests the message when plugins are installed but none match the view. + * + * @ticket 65819 + */ + public function test_should_report_no_plugins_found() { + wp_set_current_user( self::$admin_id ); + + $GLOBALS['plugins']['all'] = array( 'hello.php' => array( 'Name' => 'Hello Dolly' ) ); + + $this->assertSame( 'No plugins found.', $this->get_no_items() ); + } + + /** + * Tests that an empty search term is not treated as a search. + * + * @ticket 65819 + * + * @dataProvider data_empty_search_terms + * + * @param string $search The search term. + */ + public function test_should_ignore_empty_search_term( $search ) { + wp_set_current_user( self::$admin_id ); + + $_REQUEST['s'] = $search; + + $this->assertSame( 'No plugins are currently available.', $this->get_no_items() ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_empty_search_terms() { + return array( + 'empty string' => array( '' ), + 'zero string' => array( '0' ), + ); + } + + /** + * Tests the message and directory link for a search without results. + * + * @ticket 65819 + * + * @group ms-excluded + */ + public function test_should_report_search_without_results_with_directory_link() { + wp_set_current_user( self::$admin_id ); + + $_REQUEST['s'] = 'contact form'; + + $this->assertSame( + 'No plugins found for: contact form.' + . ' Search for plugins in the WordPress Plugin Directory.', + $this->get_no_items() + ); + } + + /** + * Tests that the search term is unslashed, decoded and escaped. + * + * @ticket 65819 + * + * @group ms-excluded + */ + public function test_should_escape_search_term() { + wp_set_current_user( self::$admin_id ); + + $_REQUEST['s'] = 'it\\\'s %3Cb%3E & "x"'; + + $this->assertSame( + 'No plugins found for: it's <b> & "x".' + . ' Search for plugins in the WordPress Plugin Directory.', + $this->get_no_items() + ); + } + + /** + * Tests that no directory link is shown to a user who cannot install plugins. + * + * @ticket 65819 + */ + public function test_should_not_link_directory_for_user_without_capability() { + wp_set_current_user( self::$subscriber_id ); + + $_REQUEST['s'] = 'contact form'; + + $this->assertSame( 'No plugins found for: contact form.', $this->get_no_items() ); + } + + /** + * Tests that no directory link is shown on multisite. + * + * @ticket 65819 + * + * @group ms-required + */ + public function test_should_not_link_directory_on_multisite() { + grant_super_admin( self::$admin_id ); + wp_set_current_user( self::$admin_id ); + + $_REQUEST['s'] = 'contact form'; + + try { + $output = $this->get_no_items(); + } finally { + revoke_super_admin( self::$admin_id ); + } + + $this->assertSame( 'No plugins found for: contact form.', $output ); + } + + /** + * Returns the output of no_items(). + * + * @return string The output. + */ + private function get_no_items() { + $table = _get_list_table( 'WP_Plugins_List_Table', array( 'screen' => 'plugins' ) ); + + return get_echo( array( $table, 'no_items' ) ); + } +} diff --git a/tests/phpunit/tests/admin/wpPostsListTable.php b/tests/phpunit/tests/admin/wpPostsListTable.php index 25324dcb4cb7f..50fd032c86265 100644 --- a/tests/phpunit/tests/admin/wpPostsListTable.php +++ b/tests/phpunit/tests/admin/wpPostsListTable.php @@ -407,6 +407,130 @@ public function test_get_views_should_return_views_by_default() { $this->assertSame( $expected, $actual ); } + /** + * @ticket 65772 + * @covers WP_Posts_List_Table::get_views + */ + public function test_get_views_should_include_scheduled_posts_link() { + $global_names = array( 'avail_post_stati', 'locked_post_status', 'post_type_object' ); + $backup = array_intersect_key( $GLOBALS, array_flip( $global_names ) ); + $future_date = gmdate( 'Y-m-d H:i:s', time() + DAY_IN_SECONDS ); + self::factory()->post->create( + array( + 'post_status' => 'future', + 'post_date' => get_date_from_gmt( $future_date ), + 'post_date_gmt' => $future_date, + ) + ); + + try { + $GLOBALS['locked_post_status'] = ''; + $GLOBALS['avail_post_stati'] = get_available_post_statuses( 'post' ); + $table = _get_list_table( 'WP_Posts_List_Table', array( 'screen' => 'edit-post' ) ); + + $views = $table->get_views(); + + $this->assertArrayHasKey( 'future', $views ); + $this->assertSame( + 'Scheduled (1)', + $views['future'] + ); + } finally { + foreach ( $global_names as $name ) { + if ( array_key_exists( $name, $backup ) ) { + $GLOBALS[ $name ] = $backup[ $name ]; + } else { + unset( $GLOBALS[ $name ] ); + } + } + } + } + + /** + * @ticket 65772 + * @covers WP_Posts_List_Table::prepare_items + */ + public function test_prepare_items_should_only_include_scheduled_posts_when_filtered() { + $global_names = array( 'mode', 'avail_post_stati', 'per_page', 'post_type_object' ); + $backup = array_intersect_key( $GLOBALS, array_flip( $global_names ) ); + $future_date = gmdate( 'Y-m-d H:i:s', time() + DAY_IN_SECONDS ); + $scheduled_id = self::factory()->post->create( + array( + 'post_status' => 'future', + 'post_date' => get_date_from_gmt( $future_date ), + 'post_date_gmt' => $future_date, + ) + ); + self::factory()->post->create( array( 'post_status' => 'publish' ) ); + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + + try { + $_GET = array( + 'post_type' => 'post', + 'post_status' => 'future', + ); + $_REQUEST = $_GET; + $table = _get_list_table( 'WP_Posts_List_Table', array( 'screen' => 'edit-post' ) ); + + $table->prepare_items(); + + $post_ids = wp_list_pluck( $GLOBALS['wp_query']->posts, 'ID' ); + $this->assertSame( array( $scheduled_id ), $post_ids ); + } finally { + foreach ( $global_names as $name ) { + if ( array_key_exists( $name, $backup ) ) { + $GLOBALS[ $name ] = $backup[ $name ]; + } else { + unset( $GLOBALS[ $name ] ); + } + } + } + } + + /** + * @ticket 65772 + * @dataProvider data_scheduled_post_date_column_status + * @covers WP_Posts_List_Table::column_date + * + * @param int $offset Seconds from now to the scheduled date. + * @param string $expected Expected status markup. + */ + public function test_column_date_should_show_scheduled_post_status( $offset, $expected ) { + $future_date = gmdate( 'Y-m-d H:i:s', time() + DAY_IN_SECONDS ); + $post = self::factory()->post->create_and_get( + array( + 'post_status' => 'future', + 'post_date' => get_date_from_gmt( $future_date ), + 'post_date_gmt' => $future_date, + ) + ); + + // Model an overdue date without letting wp_update_post() publish the fixture. + $date = gmdate( 'Y-m-d H:i:s', time() + $offset ); + $post->post_date = get_date_from_gmt( $date ); + $post->post_date_gmt = $date; + + ob_start(); + try { + $this->table->column_date( $post ); + $output = ob_get_contents(); + } finally { + ob_end_clean(); + } + + $this->assertStringStartsWith( $expected . '
    ', $output ); + } + + /** + * @return array[] + */ + public static function data_scheduled_post_date_column_status() { + return array( + 'future date' => array( DAY_IN_SECONDS, 'Scheduled' ), + 'past due date' => array( -DAY_IN_SECONDS, 'Missed schedule' ), + ); + } + /** * Renders the title column for a post in a given list view mode. * @@ -467,7 +591,7 @@ public function test_no_title_excerpt_visibility( $mode_value, $post_args, $shou * * @return array[] */ - public function data_no_title_excerpt_visibility() { + public static function data_no_title_excerpt_visibility() { return array( 'compact view, untitled' => array( 'list', array(), true ), 'extended view, untitled' => array( 'excerpt', array(), false ), diff --git a/tests/phpunit/tests/admin/wpPrivacyRequestsTable.php b/tests/phpunit/tests/admin/wpPrivacyRequestsTable.php index 66e3e02501cfb..2ea649bc74cb8 100644 --- a/tests/phpunit/tests/admin/wpPrivacyRequestsTable.php +++ b/tests/phpunit/tests/admin/wpPrivacyRequestsTable.php @@ -129,7 +129,7 @@ public function filter_posts_request( $request ) { * } * } */ - public function data_columns_should_be_sortable() { + public static function data_columns_should_be_sortable() { return array( // Default order (ID) DESC. array( diff --git a/tests/phpunit/tests/admin/wpSiteHealth.php b/tests/phpunit/tests/admin/wpSiteHealth.php index 6080b477f54c3..2ad2052369ba1 100644 --- a/tests/phpunit/tests/admin/wpSiteHealth.php +++ b/tests/phpunit/tests/admin/wpSiteHealth.php @@ -126,7 +126,7 @@ public function test_cron_health_checks( $times, $expected_status, $expected_lab * 4. bool Expected outcome has_late_cron() * 5. bool Expected outcome has_missed_cron() */ - public function data_cron_health_checks() { + public static function data_cron_health_checks() { return array( array( '+5 minutes', @@ -269,7 +269,7 @@ function ( $response, $parsed_args ) use ( &$responses, &$is_unauthorized, $has_ * delay_the_response?: bool, * }> */ - public function data_get_page_cache(): array { + public static function data_get_page_cache(): array { $recommended_label = 'Page cache is not detected but the server response time is OK'; $good_label = 'Page cache is detected and the server response time is good'; $critical_label = 'Page cache is not detected and the server response time is slow'; @@ -561,7 +561,7 @@ static function ( $thresholds ) use ( $threshold, $count ) { * * @ticket 56040 */ - public function data_object_cache_thresholds() { + public static function data_object_cache_thresholds() { return array( array( 'comments_count', 0 ), array( 'posts_count', 0 ), diff --git a/tests/phpunit/tests/admin/wpSiteHealthDetectPluginThemeAutoUpdateIssues.php b/tests/phpunit/tests/admin/wpSiteHealthDetectPluginThemeAutoUpdateIssues.php new file mode 100644 index 0000000000000..391de1ae0e331 --- /dev/null +++ b/tests/phpunit/tests/admin/wpSiteHealthDetectPluginThemeAutoUpdateIssues.php @@ -0,0 +1,141 @@ +add_value_filter( 'auto_update_plugin', $plugin_updates ); + $this->add_value_filter( 'auto_update_theme', $theme_updates ); + $this->add_value_filter( 'plugins_auto_update_enabled', $plugins_ui ); + $this->add_value_filter( 'themes_auto_update_enabled', $themes_ui ); + + $site_health = new WP_Site_Health(); + + $result = $site_health->detect_plugin_theme_auto_update_issues(); + + $this->assertIsObject( $result, 'The result should be an object.' ); + $this->assertSame( + array( + 'status' => $status, + 'message' => $message, + ), + (array) $result, + 'The status and message should match.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_auto_update_states() { + $critical = 'Auto-updates for plugins and/or themes appear to be disabled, but settings are still set to be displayed. This could cause auto-updates to not work as expected.'; + $both = 'Auto-updates for plugins and themes appear to be disabled. This will prevent your site from receiving new versions automatically when available.'; + $plugins = 'Auto-updates for plugins appear to be disabled. This will prevent your site from receiving new versions automatically when available.'; + $themes = 'Auto-updates for themes appear to be disabled. This will prevent your site from receiving new versions automatically when available.'; + $good = 'There appear to be no issues with plugin and theme auto-updates.'; + + return array( + 'plugin updates off, plugins UI on' => array( false, null, true, false, 'critical', $critical ), + 'theme updates off, themes UI on' => array( null, false, false, true, 'critical', $critical ), + 'both off, both UIs on' => array( false, false, true, true, 'critical', $critical ), + 'both off, UIs off' => array( false, false, false, false, 'recommended', $both ), + 'plugin updates off, UIs off' => array( false, null, false, false, 'recommended', $plugins ), + 'plugin updates off, theme updates on' => array( false, true, false, false, 'recommended', $plugins ), + 'theme updates off, UIs off' => array( null, false, false, false, 'recommended', $themes ), + 'theme updates off, plugin updates on' => array( true, false, false, false, 'recommended', $themes ), + 'no filters, UIs off' => array( null, null, false, false, 'good', $good ), + 'no filters, UIs on' => array( null, null, true, true, 'good', $good ), + 'both forced on, UIs on' => array( true, true, true, true, 'good', $good ), + 'plugin updates off, UIs on for themes' => array( false, null, false, true, 'recommended', $plugins ), + ); + } + + /** + * Tests that the auto-update filters receive an available update for a fake plugin and theme. + * + * @ticket 65819 + */ + public function test_should_pass_mock_items_to_auto_update_filters() { + $plugin_filter = new MockAction(); + $theme_filter = new MockAction(); + add_filter( 'auto_update_plugin', array( $plugin_filter, 'filter' ), 10, 2 ); + add_filter( 'auto_update_theme', array( $theme_filter, 'filter' ), 10, 2 ); + + $site_health = new WP_Site_Health(); + $site_health->detect_plugin_theme_auto_update_issues(); + + $plugin_args = $plugin_filter->get_args(); + $theme_args = $theme_filter->get_args(); + + $this->assertTrue( $plugin_args[0][0], 'The plugin filter should receive true.' ); + $this->assertSame( 'a-fake-plugin/a-fake-plugin.php', $plugin_args[0][1]->plugin, 'The plugin filter should receive the fake plugin.' ); + $this->assertTrue( $theme_args[0][0], 'The theme filter should receive true.' ); + $this->assertSame( 'a-fake-theme', $theme_args[0][1]->theme, 'The theme filter should receive the fake theme.' ); + } + + /** + * Adds a filter that returns the given value. + * + * @param string $hook The filter name. + * @param bool|null $value The value to return, or null for no filter. + */ + private function add_value_filter( $hook, $value ) { + if ( null === $value ) { + return; + } + + add_filter( $hook, $value ? '__return_true' : '__return_false' ); + } +} diff --git a/tests/phpunit/tests/admin/wpSiteHealthGetTestPluginVersion.php b/tests/phpunit/tests/admin/wpSiteHealthGetTestPluginVersion.php new file mode 100644 index 0000000000000..3aad5118d1bf5 --- /dev/null +++ b/tests/phpunit/tests/admin/wpSiteHealthGetTestPluginVersion.php @@ -0,0 +1,275 @@ +user->create( array( 'role' => 'administrator' ) ); + + if ( is_multisite() ) { + grant_super_admin( self::$admin_id ); + } + } + + /** + * Revokes the super admin grant before the fixture users are deleted. + */ + public static function wpTearDownAfterClass() { + if ( is_multisite() ) { + revoke_super_admin( self::$admin_id ); + } + } + + /** + * Resets the plugins cache, the active plugins and the update transient. + */ + public function set_up() { + parent::set_up(); + + wp_set_current_user( self::$admin_id ); + + update_option( 'active_plugins', array() ); + update_site_option( 'active_sitewide_plugins', array() ); + delete_site_transient( 'update_plugins' ); + } + + /** + * Clears the plugins cache and the update transient. + */ + public function tear_down() { + wp_cache_delete( 'plugins', 'plugins' ); + delete_site_transient( 'update_plugins' ); + + parent::tear_down(); + } + + /** + * Tests the result when every installed plugin is active and up to date. + * + * @ticket 65819 + * + * @dataProvider data_active_plugins + * + * @param int $count The number of installed, active plugins. + * @param string $sentence The expected sentence. + */ + public function test_should_report_active_plugins_up_to_date( $count, $sentence ) { + $this->set_plugins( $count, $count ); + + $this->assertSame( + array( + 'label' => 'Your plugins are all up to date', + 'status' => 'good', + 'badge' => array( + 'label' => 'Security', + 'color' => 'blue', + ), + 'description' => '

    Plugins extend your site’s functionality with things like contact forms, ecommerce and much more. That means they have deep access to your site, so it’s vital to keep them up to date.

    ' . $sentence . '

    ', + 'actions' => '

    Manage your plugins

    ', + 'test' => 'plugin_version', + ), + $this->get_result() + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_active_plugins() { + return array( + 'one plugin' => array( 1, 'Your site has 1 active plugin, and it is up to date.' ), + 'two plugins' => array( 2, 'Your site has 2 active plugins, and they are all up to date.' ), + ); + } + + /** + * Tests the result when active plugins have updates waiting. + * + * @ticket 65819 + * + * @dataProvider data_outdated_plugins + * + * @param int $outdated The number of plugins with an update. + * @param string $sentence The expected sentence. + */ + public function test_should_report_outdated_plugins( $outdated, $sentence ) { + $this->set_plugins( 2, 2, $outdated ); + + $result = $this->get_result(); + + $this->assertSame( 'critical', $result['status'], 'The status should be critical.' ); + $this->assertSame( 'You have plugins waiting to be updated', $result['label'], 'The label should match.' ); + $this->assertStringEndsWith( '

    ' . $sentence . '

    ', $result['description'], 'The description should count the outdated plugins.' ); + $this->assertSame( + '

    Manage your plugins

    ' + . '

    Update your plugins

    ', + $result['actions'], + 'The actions should link to the plugin updates.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_outdated_plugins() { + return array( + 'one outdated plugin' => array( 1, 'Your site has 1 plugin waiting to be updated.' ), + 'two outdated plugins' => array( 2, 'Your site has 2 plugins waiting to be updated.' ), + ); + } + + /** + * Tests that an update for a plugin that is not installed is ignored. + * + * @ticket 65819 + */ + public function test_should_ignore_updates_for_plugins_not_installed() { + $this->set_plugins( 1, 1 ); + + $update_plugins = get_site_transient( 'update_plugins' ); + $update_plugins->response['missing/missing.php'] = (object) array( + 'slug' => 'missing', + 'new_version' => '2.0', + ); + set_site_transient( 'update_plugins', $update_plugins ); + + $this->assertSame( 'good', $this->get_result()['status'] ); + } + + /** + * Tests the result when some installed plugins are inactive. + * + * @ticket 65819 + * + * @group ms-excluded + * + * @dataProvider data_inactive_plugins + * + * @param int $total The number of installed plugins. + * @param int $active The number of active plugins. + * @param string $sentence The expected sentence about inactive plugins. + */ + public function test_should_recommend_removing_inactive_plugins( $total, $active, $sentence ) { + $this->set_plugins( $total, $active ); + + $result = $this->get_result(); + + $this->assertSame( 'recommended', $result['status'], 'The status should be recommended.' ); + $this->assertSame( 'You should remove inactive plugins', $result['label'], 'The label should match.' ); + $this->assertStringEndsWith( + '

    ' . $sentence . ' Inactive plugins are tempting targets for attackers. If you are not going to use a plugin, you should consider removing it.

    ', + $result['description'], + 'The description should count the inactive plugins.' + ); + $this->assertStringEndsWith( + '

    Manage inactive plugins

    ', + $result['actions'], + 'The actions should link to the inactive plugins.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_inactive_plugins() { + return array( + 'one inactive plugin' => array( 2, 1, 'Your site has 1 inactive plugin.' ), + 'two inactive plugins' => array( 3, 1, 'Your site has 2 inactive plugins.' ), + ); + } + + /** + * Tests that inactive plugins are not reported on multisite. + * + * @ticket 65819 + * + * @group ms-required + */ + public function test_should_not_report_inactive_plugins_on_multisite() { + $this->set_plugins( 2, 1 ); + + $result = $this->get_result(); + + $this->assertSame( 'good', $result['status'], 'The status should stay good.' ); + $this->assertStringEndsWith( '

    Your site has 1 active plugin, and it is up to date.

    ', $result['description'], 'Only the active plugin should be counted, and inactive plugins should not be mentioned.' ); + } + + /** + * Primes the plugins cache with test plugins, activates some and adds updates. + * + * @param int $total The number of installed plugins. + * @param int $active The number of active plugins, from the first. + * @param int $outdated The number of plugins with an update, from the first. + */ + private function set_plugins( $total, $active, $outdated = 0 ) { + $plugins = array(); + $response = array(); + + for ( $i = 1; $i <= $total; $i++ ) { + $file = "plugin-{$i}/plugin-{$i}.php"; + + $plugins[ $file ] = array( + 'Name' => "Plugin {$i}", + 'Version' => '1.0', + ); + + if ( $i <= $outdated ) { + $response[ $file ] = (object) array( + 'slug' => "plugin-{$i}", + 'new_version' => '2.0', + ); + } + } + + wp_cache_set( 'plugins', array( '' => $plugins ), 'plugins' ); + + $active_plugins = array_slice( array_keys( $plugins ), 0, $active ); + if ( is_multisite() ) { + update_site_option( 'active_sitewide_plugins', array_fill_keys( $active_plugins, time() ) ); + } else { + update_option( 'active_plugins', $active_plugins ); + } + + set_site_transient( 'update_plugins', (object) array( 'response' => $response ) ); + } + + /** + * Runs the test. + * + * @return array The test result. + */ + private function get_result() { + $site_health = new WP_Site_Health(); + + return $site_health->get_test_plugin_version(); + } +} diff --git a/tests/phpunit/tests/admin/wpUpgrader.php b/tests/phpunit/tests/admin/wpUpgrader.php index ffea594ae87bc..78744f774d35b 100644 --- a/tests/phpunit/tests/admin/wpUpgrader.php +++ b/tests/phpunit/tests/admin/wpUpgrader.php @@ -49,11 +49,11 @@ public static function set_up_before_class() { public function set_up() { parent::set_up(); - self::$upgrader_skin_mock = $this->getMockBuilder( 'WP_Upgrader_Skin' )->getMock(); + self::$upgrader_skin_mock = $this->createMock( 'WP_Upgrader_Skin' ); self::$instance = new WP_Upgrader( self::$upgrader_skin_mock ); - self::$wp_filesystem_mock = $this->getMockBuilder( 'WP_Filesystem_Base' )->getMock(); + self::$wp_filesystem_mock = $this->createMock( 'WP_Filesystem_Base' ); if ( array_key_exists( 'wp_filesystem', $GLOBALS ) ) { self::$wp_filesystem_backup = $GLOBALS['wp_filesystem']; @@ -126,7 +126,7 @@ public function test_init_should_initialize_strings( $key ) { * * @return array[] */ - public function data_init_should_initialize_strings() { + public static function data_init_should_initialize_strings() { return self::text_array_to_dataprovider( array( 'bad_request', @@ -186,7 +186,7 @@ public function test_flatten_dirlist_should_flatten_the_provided_directory_list( * * @return array[] */ - public function data_should_flatten_dirlist() { + public static function data_should_flatten_dirlist() { return array( 'empty array, default path' => array( 'expected' => array(), @@ -780,7 +780,7 @@ public function test_install_package_should_return_wp_error_with_invalid_destina * * @return array[] */ - public function data_install_package_invalid_paths() { + public static function data_install_package_invalid_paths() { return array( 'empty string' => array( 'path' => '' ), @@ -1272,7 +1272,7 @@ function ( $removed, $local_destination, $remote_destination ) use ( $expected ) * * @return array[] */ - public function data_install_package_should_make_remote_destination_safe_when_set_to_a_protected_directory() { + public static function data_install_package_should_make_remote_destination_safe_when_set_to_a_protected_directory() { return array( 'ABSPATH' => array( 'protected_directory' => ABSPATH, diff --git a/tests/phpunit/tests/admin/wpUpgraderSkin.php b/tests/phpunit/tests/admin/wpUpgraderSkin.php new file mode 100644 index 0000000000000..a4746e78a7375 --- /dev/null +++ b/tests/phpunit/tests/admin/wpUpgraderSkin.php @@ -0,0 +1,123 @@ +setAccessible( true ); + } + + return get_echo( array( $method, 'invoke' ), array( $skin, $type ) ); + } + + /** + * Tests that `WP_Upgrader_Skin::decrement_update_count()` prints nothing + * when there is no usable result. + * + * @ticket 59446 + * + * @dataProvider data_results_that_should_print_nothing + * + * @covers WP_Upgrader_Skin::decrement_update_count + * + * @param mixed $result The result to set on the skin before calling the method. + */ + public function test_decrement_update_count_should_print_nothing_without_a_usable_result( $result ) { + $skin = new WP_Upgrader_Skin(); + $skin->result = $result; // @phpstan-ignore assign.propertyType (Invalid assignment on purpose to test.) + + $this->assertSame( '', $this->decrement_update_count( $skin, 'plugin' ) ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_results_that_should_print_nothing(): array { + return array( + 'a false result' => array( 'result' => false ), + 'a WP_Error result' => array( 'result' => new WP_Error( 'test_error' ) ), + "an 'up_to_date' result" => array( 'result' => 'up_to_date' ), + ); + } + + /** + * Tests that `WP_Upgrader_Skin::decrement_update_count()` prints its data + * through `wp_print_inline_script_tag()`, so attributes such as a + * per-request nonce can be attached via the `wp_inline_script_attributes` + * filter, and that the update type is JSON-encoded. + * + * @ticket 59446 + * + * @dataProvider data_update_types + * + * @covers WP_Upgrader_Skin::decrement_update_count + * + * @param non-falsy-string $type The update type to pass to the method. + */ + public function test_decrement_update_count_prints_inline_script_tag_with_filterable_attributes( string $type ) { + $nonce = 'test-decrement-update-count-nonce'; + add_filter( + 'wp_inline_script_attributes', + static function ( array $attributes ) use ( $nonce ): array { + $attributes['nonce'] = $nonce; + return $attributes; + } + ); + + $skin = new WP_Upgrader_Skin(); + $skin->result = true; + + $actual = $this->decrement_update_count( $skin, $type ); + + $processor = new WP_HTML_Tag_Processor( $actual ); + $this->assertTrue( $processor->next_tag( 'SCRIPT' ), 'The expected SCRIPT tag was not printed.' ); + $this->assertSame( $nonce, $processor->get_attribute( 'nonce' ), 'The nonce attribute added via wp_inline_script_attributes was not printed.' ); + + $script_text = $processor->get_modifiable_text(); + $this->assertStringContainsString( 'wp.updates.decrementCount( upgradeType )', $script_text, 'The expected JavaScript call was not printed.' ); + $this->assertStringEndsWith( + ')( ' . wp_json_encode( $type, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) . ' );', + trim( $script_text ), + 'The upgrade type was not passed as the JSON-encoded argument.' + ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_update_types(): array { + return array( + 'a plugin type' => array( 'type' => 'plugin' ), + 'a theme type' => array( 'type' => 'theme' ), + 'a translation type' => array( 'type' => 'translation' ), + ); + } +} diff --git a/tests/phpunit/tests/adminbar.php b/tests/phpunit/tests/adminbar.php index fb29eaf39af42..95c479ceb8bdc 100644 --- a/tests/phpunit/tests/adminbar.php +++ b/tests/phpunit/tests/adminbar.php @@ -20,6 +20,13 @@ public static function set_up_before_class() { require_once ABSPATH . WPINC . '/class-wp-admin-bar.php'; } + public function set_up() { + parent::set_up(); + + // is_admin_bar_showing() caches its result, so an earlier logged-out test can leave it false. + unset( $GLOBALS['show_admin_bar'] ); + } + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) ); self::$user_ids[] = self::$editor_id; @@ -904,6 +911,58 @@ public function test_site_name_menu_has_no_site_icon_in_user_admin() { $this->assertArrayNotHasKey( 'class', $node_site_name->meta ); } + /** + * @ticket 66157 + * + * @covers ::wp_admin_bar_my_account_item + * @covers ::wp_admin_bar_my_account_menu + */ + public function test_my_account_has_greeting_in_user_info_only() { + $user_id = self::factory()->user->create( + array( + 'role' => 'editor', + 'display_name' => 'John Doe', + ) + ); + wp_set_current_user( $user_id ); + $current_user = wp_get_current_user(); + + $wp_admin_bar = $this->get_standard_admin_bar(); + $node_my_account = $wp_admin_bar->get_node( 'my-account' ); + $node_user_info = $wp_admin_bar->get_node( 'user-info' ); + + $this->assertSame( 'John Doe', wp_strip_all_tags( $node_my_account->title ) ); + $this->assertSame( 'John Doe', $node_my_account->meta['menu_title'] ); + + $this->assertSame( 'Howdy, John Doe' . $current_user->user_login . 'Edit Profile', wp_strip_all_tags( $node_user_info->title ) ); + $this->assertStringContainsString( "", $node_user_info->title ); + } + + /** + * @ticket 66157 + * + * @covers ::wp_admin_bar_my_account_item + * @covers ::wp_admin_bar_my_account_menu + */ + public function test_my_account_falls_back_to_user_login_when_display_name_is_empty() { + add_filter( 'pre_user_display_name', '__return_empty_string' ); + $user_id = self::factory()->user->create( array( 'role' => 'editor' ) ); + + wp_set_current_user( $user_id ); + $current_user = wp_get_current_user(); + $this->assertSame( '', $current_user->display_name ); + + $wp_admin_bar = $this->get_standard_admin_bar(); + $node_my_account = $wp_admin_bar->get_node( 'my-account' ); + $node_user_info = $wp_admin_bar->get_node( 'user-info' ); + + $this->assertSame( $current_user->user_login, wp_strip_all_tags( $node_my_account->title ) ); + $this->assertSame( $current_user->user_login, $node_my_account->meta['menu_title'] ); + + $this->assertSame( 'Howdy, ' . $current_user->user_login . $current_user->user_login . 'Edit Profile', wp_strip_all_tags( $node_user_info->title ) ); + $this->assertStringContainsString( "