diff --git a/.env b/.env index 5b30ab5..b2afa7a 100644 --- a/.env +++ b/.env @@ -52,8 +52,8 @@ OPENAI_API_KEY=!ChangeMe! ###> symfony/messenger ### # Choose one of the transports below # MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages -# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages -MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 +MESSENGER_TRANSPORT_DSN=redis://redis.app-sf.orb.local:6379/messages +#MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 ###< symfony/messenger ### # SQL Runner @@ -62,3 +62,11 @@ SQLRUNNER_URL=http://sqlrunner.app-sf.orb.local:8080 ###> symfony/line-notify-notifier ### # LINE_NOTIFY_DSN=linenotify://TOKEN@default ###< symfony/line-notify-notifier ### + +###> symfony/amazon-mailer ### +# MAILER_DSN=ses://ACCESS_KEY:SECRET_KEY@default?region=eu-west-1 +# MAILER_DSN=ses+smtp://ACCESS_KEY:SECRET_KEY@default?region=eu-west-1 +###< symfony/amazon-mailer ### + +# Email +SERVER_EMAIL=no-reply@dbplay.pan93.com diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 0d55d27..500999d 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -1,10 +1,10 @@ -name: Create and Publish app-sf image +name: Create and Publish app-sf and worker image on: push: branches: - - master - - distro/* + - "*" + - "*/*" tags: - v* workflow_dispatch: @@ -17,12 +17,64 @@ permissions: env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} jobs: - docker-image: + docker-app-sf: runs-on: ubuntu-latest + env: + # app-sf + IMAGE_NAME: ${{ github.repository }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@master + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@master + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build Docker image + uses: docker/build-push-action@master + with: + context: . + push: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Conditional push for main branch + if: startsWith(github.ref, 'refs/heads/master') || startsWith(github.ref, 'refs/heads/distro/') || startsWith(github.ref, 'refs/tags/v') + uses: docker/build-push-action@master + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Generate artifact attestation + if: startsWith(github.ref, 'refs/heads/master') || startsWith(github.ref, 'refs/heads/distro/') || startsWith(github.ref, 'refs/tags/v') + uses: actions/attest-build-provenance@main + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + + docker-app-sf-worker: + runs-on: ubuntu-latest + + env: + # app-sf-worker + IMAGE_NAME: ${{ github.repository }}-worker + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -40,18 +92,29 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - name: Build and push Docker image - id: push + - name: Build Docker image + uses: docker/build-push-action@master + with: + file: worker.Dockerfile + context: . + push: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Conditional push for main branch + if: startsWith(github.ref, 'refs/heads/master') || startsWith(github.ref, 'refs/heads/distro/') || startsWith(github.ref, 'refs/tags/v') uses: docker/build-push-action@master with: + file: worker.Dockerfile context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - name: Generate artifact attestation + if: startsWith(github.ref, 'refs/heads/master') || startsWith(github.ref, 'refs/heads/distro/') || startsWith(github.ref, 'refs/tags/v') uses: actions/attest-build-provenance@main with: - subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true diff --git a/.gitignore b/.gitignore index 226372d..50db1a6 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ phpstan.neon ###< phpstan/phpstan ### # node_modules -node_modules/ \ No newline at end of file +node_modules/ +.pnpm-store/ \ No newline at end of file diff --git a/.idea/app-sf.iml b/.idea/app-sf.iml index 82e1e39..0fc493a 100644 --- a/.idea/app-sf.iml +++ b/.idea/app-sf.iml @@ -5,7 +5,10 @@ + + + @@ -199,6 +202,9 @@ + + + diff --git a/.idea/php.xml b/.idea/php.xml index aa2a1b7..56839c4 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -219,6 +219,9 @@ + + + diff --git a/README.md b/README.md index 4215281..eb785d0 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,11 @@ The Database Playground is a platform designed to enhance your SQL skills throug 1. Deploy Redis, PostgreSQL, Meilisearch, and Umami (for statistics) on Zeabur. 2. Deploy [SQL runner](https://github.com/database-playground/sqlrunner-v2) on Zeabur, and rename the service host to `sqlrunner`. -3. Deploy the application in Git mode on Zeabur. -4. Add the following environment variables to the application: +3. Deploy the application in Git mode on Zeabur. You can use our prebuilt image at + the GitHub Registry. +4. Deploy the worker in Git mode on Zeabur. You can use our prebuilt image at + the GitHub Registry. Also, it is recommended to create more than 1 worker. +5. Add the following environment variables to the application: ```env DATABASE_URL=postgresql://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@postgresql.zeabur.internal:5432/${POSTGRES_DATABASE}?serverVersion=16&charset=utf8 REDIS_URI=${REDIS_CONNECTION_STRING} @@ -56,14 +59,28 @@ The Database Playground is a platform designed to enhance your SQL skills throug OPENAI_API_KEY=your-openai-api-key LINE_NOTIFY_DSN=linenotify://line-notify-token@default SQLRUNNER_URL=http://sqlrunner.zeabur.internal:8080 + MAILER_DSN=ses://ACCESS_KEY:SECRET_KEY@default?region=eu-west-1 + MESSENGER_TRANSPORT_DSN=${REDIS_URI}/messages ``` -5. Bind your domain, and the application will be ready for use. The Meilisearch index will be automatically created on start up. +6. Add the following environment variables to the worker: + ```env + DATABASE_URL=postgresql://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@postgresql.zeabur.internal:5432/${POSTGRES_DATABASE}?serverVersion=16&charset=utf8 + REDIS_URI=${REDIS_CONNECTION_STRING} + MEILISEARCH_URL=http://meilisearch.zeabur.internal:7700 + MEILISEARCH_API_KEY=${MEILI_MASTER_KEY} + APP_SECRET=${PASSWORD} + LINE_NOTIFY_DSN=linenotify://line-notify-token@default + MAILER_DSN=ses://ACCESS_KEY:SECRET_KEY@default?region=eu-west-1 + MESSENGER_TRANSPORT_DSN=${REDIS_URI}/messages + MESSENGER_CONSUMER_NAME=app-sf-worker-1 # Change the number for each worker + ``` +7. Bind your domain, and the application will be ready for use. The Meilisearch index will be automatically created on start up. ### Docker We provide a Docker Compose configuration based on [Symfony Docker](https://github.com/dunglas/symfony-docker) for deployment. The prebuilt image is available at -the [GitHub Registry](https://github.com/database-playground/app-sf/pkgs/container/app-sf). +the [GitHub Registry](https://github.com/orgs/database-playground/packages). To deploy the application, you may need to update the secret or environment variables in the `compose.yaml` and `compose.prod.yaml` files, and then run the following command: diff --git a/assets/styles/app.scss b/assets/styles/app.scss index a3a504e..1f41fc1 100644 --- a/assets/styles/app.scss +++ b/assets/styles/app.scss @@ -413,3 +413,10 @@ ul.credit { } } } + +.app-email-preview { + &__rendered__content__html { + height: 60vh; + width: 100%; + } +} diff --git a/compose.override.yaml b/compose.override.yaml index ccc7610..ca0fd6b 100644 --- a/compose.override.yaml +++ b/compose.override.yaml @@ -26,4 +26,8 @@ services: environment: MP_SMTP_AUTH_ACCEPT_ANY: 1 MP_SMTP_AUTH_ALLOW_INSECURE: 1 -###< symfony/mailer ### + ###< symfony/mailer ### + + worker: + env_file: + - .env.local diff --git a/compose.yaml b/compose.yaml index d9360ce..dcaa8a2 100644 --- a/compose.yaml +++ b/compose.yaml @@ -46,7 +46,7 @@ services: sqlrunner: image: ghcr.io/database-playground/sqlrunner-v2:main php: - image: ${IMAGES_PREFIX:-}app-php + image: ${IMAGES_PREFIX:-}app-sf restart: unless-stopped environment: SERVER_NAME: ${SERVER_NAME:-localhost}, php:80 @@ -55,6 +55,7 @@ services: MEILISEARCH_URL: "http://meilisearch:7700" MEILISEARCH_API_KEY: ${MEILI_MASTER_KEY:-!MasterChangeMe!} SQLRUNNER_URL: "http://sqlrunner:8080" + MESSENGER_TRANSPORT_DSN: "${REDIS_URI}/messages" volumes: - caddy_data:/data - caddy_config:/config @@ -72,6 +73,27 @@ services: - target: 443 published: ${HTTP3_PORT:-443} protocol: udp + worker: + image: ${IMAGES_PREFIX:-}app-sf-worker + build: + dockerfile: worker.Dockerfile + context: . + environment: + DATABASE_URL: "postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-16}&charset=${POSTGRES_CHARSET:-utf8}" + REDIS_URI: "redis://redis:6379" + MEILISEARCH_URL: "http://meilisearch:7700" + MEILISEARCH_API_KEY: ${MEILI_MASTER_KEY:-!MasterChangeMe!} + MESSENGER_TRANSPORT_DSN: "${REDIS_URI}/messages" + MESSENGER_CONSUMER_NAME: app-sf-worker-1 + restart: unless-stopped + depends_on: + php: + condition: service_healthy + worker-2: + extends: + service: worker + environment: + MESSENGER_CONSUMER_NAME: app-sf-worker-2 volumes: ###> doctrine/doctrine-bundle ### diff --git a/composer.json b/composer.json index af12525..2c5f0ba 100644 --- a/composer.json +++ b/composer.json @@ -20,12 +20,14 @@ "jblond/php-diff": "dev-master", "league/commonmark": "dev-main", "meilisearch/search-bundle": "dev-main", + "notfloran/mjml-bundle": "dev-main", "nyholm/psr7": "dev-master", "openai-php/client": "dev-main", "oro/doctrine-extensions": "dev-master", "phpdocumentor/reflection-docblock": "5.*", "runtime/frankenphp-symfony": "dev-main", "sensiolabs/typescript-bundle": "dev-main", + "symfony/amazon-mailer": "7.3.*", "symfony/asset": "7.3.*", "symfony/asset-mapper": "7.3.*", "symfony/console": "7.3.*", @@ -45,6 +47,7 @@ "symfony/notifier": "7.3.*", "symfony/password-hasher": "7.3.*", "symfony/process": "7.3.*", + "symfony/redis-messenger": "7.3.*", "symfony/runtime": "7.3.*", "symfony/security-bundle": "7.3.*", "symfony/serializer": "7.3.*", diff --git a/composer.lock b/composer.lock index f65026a..0998723 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,135 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f459ad30f7f54194e3f29cb05fec14fb", + "content-hash": "144a7186cc53f1b1d649dd70a9072b03", "packages": [ + { + "name": "async-aws/core", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/async-aws/core.git", + "reference": "3c62fe4e3ae5572fdcd2d1e80659747454c5a0f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/async-aws/core/zipball/3c62fe4e3ae5572fdcd2d1e80659747454c5a0f9", + "reference": "3c62fe4e3ae5572fdcd2d1e80659747454c5a0f9", + "shasum": "" + }, + "require": { + "ext-hash": "*", + "ext-json": "*", + "ext-simplexml": "*", + "php": "^7.2.5 || ^8.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/http-client": "^4.4.16 || ^5.1.7 || ^6.0 || ^7.0", + "symfony/http-client-contracts": "^1.1.8 || ^2.0 || ^3.0", + "symfony/service-contracts": "^1.0 || ^2.0 || ^3.0" + }, + "conflict": { + "async-aws/s3": "<1.1", + "symfony/http-client": "5.2.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.23-dev" + } + }, + "autoload": { + "psr-4": { + "AsyncAws\\Core\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Core package to integrate with AWS. This is a lightweight AWS SDK provider by AsyncAws.", + "keywords": [ + "amazon", + "async-aws", + "aws", + "sdk", + "sts" + ], + "support": { + "source": "https://github.com/async-aws/core/tree/master" + }, + "funding": [ + { + "url": "https://github.com/jderusse", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-11-24T17:57:34+00:00" + }, + { + "name": "async-aws/ses", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/async-aws/ses.git", + "reference": "974b35581d495974eed4c4bbdb31b70ea0061bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/async-aws/ses/zipball/974b35581d495974eed4c4bbdb31b70ea0061bf8", + "reference": "974b35581d495974eed4c4bbdb31b70ea0061bf8", + "shasum": "" + }, + "require": { + "async-aws/core": "^1.9", + "ext-json": "*", + "php": "^7.2.5 || ^8.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "AsyncAws\\Ses\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "SES client, part of the AWS SDK provided by AsyncAws.", + "keywords": [ + "amazon", + "async-aws", + "aws", + "sdk", + "ses" + ], + "support": { + "source": "https://github.com/async-aws/ses/tree/master" + }, + "funding": [ + { + "url": "https://github.com/jderusse", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-11-24T17:57:34+00:00" + }, { "name": "composer/semver", "version": "dev-main", @@ -464,25 +591,23 @@ "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "0d0c25d81be87f7143d1dfc635974d255e26291c" + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/0d0c25d81be87f7143d1dfc635974d255e26291c", - "reference": "0d0c25d81be87f7143d1dfc635974d255e26291c", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9", - "phpstan/phpstan": "1.4.10 || 1.10.15", - "phpstan/phpstan-phpunit": "^1.0", + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "1.4.10 || 2.0.3", + "phpstan/phpstan-phpunit": "^1.0 || ^2", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psalm/plugin-phpunit": "0.18.4", - "psr/log": "^1 || ^2 || ^3", - "vimeo/psalm": "4.30.0 || 5.12.0" + "psr/log": "^1 || ^2 || ^3" }, "suggest": { "psr/log": "Allows logging deprecations via PSR-3 logger implementation" @@ -502,9 +627,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.x" + "source": "https://github.com/doctrine/deprecations/tree/1.1.4" }, - "time": "2024-12-01T07:04:16+00:00" + "time": "2024-12-07T21:18:45+00:00" }, { "name": "doctrine/doctrine-bundle", @@ -904,12 +1029,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "9e3ae34de52dd65590c4277d5cdde8f39f12418b" + "reference": "a9e64e5ea80184e14a66c262e5bcf3c2cb4a94d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/9e3ae34de52dd65590c4277d5cdde8f39f12418b", - "reference": "9e3ae34de52dd65590c4277d5cdde8f39f12418b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a9e64e5ea80184e14a66c262e5bcf3c2cb4a94d7", + "reference": "a9e64e5ea80184e14a66c262e5bcf3c2cb4a94d7", "shasum": "" }, "require": { @@ -922,8 +1047,7 @@ "phpbench/phpbench": "^1.2", "phpstan/phpstan": "^1.9.4", "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.4" + "phpunit/phpunit": "^10.5" }, "default-branch": true, "type": "library", @@ -967,7 +1091,7 @@ "type": "tidelift" } ], - "time": "2024-11-25T19:21:52+00:00" + "time": "2024-12-02T21:34:17+00:00" }, { "name": "doctrine/lexer", @@ -1155,12 +1279,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "50d7a0f95ea6b8623589a90a2c51ddb7389f71c2" + "reference": "a15543a2cee2d548e94a3a867b4ca6cf1354cfc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/50d7a0f95ea6b8623589a90a2c51ddb7389f71c2", - "reference": "50d7a0f95ea6b8623589a90a2c51ddb7389f71c2", + "url": "https://api.github.com/repos/doctrine/orm/zipball/a15543a2cee2d548e94a3a867b4ca6cf1354cfc3", + "reference": "a15543a2cee2d548e94a3a867b4ca6cf1354cfc3", "shasum": "" }, "require": { @@ -1184,13 +1308,12 @@ "phpbench/phpbench": "^1.0", "phpdocumentor/guides-cli": "^1.4", "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "1.12.6", - "phpstan/phpstan-deprecation-rules": "^1.2", + "phpstan/phpstan": "2.0.3", + "phpstan/phpstan-deprecation-rules": "^2", "phpunit/phpunit": "^10.4.0", "psr/log": "^1 || ^2 || ^3", "squizlabs/php_codesniffer": "3.7.2", - "symfony/cache": "^5.4 || ^6.2 || ^7.0", - "vimeo/psalm": "5.26.1" + "symfony/cache": "^5.4 || ^6.2 || ^7.0" }, "suggest": { "ext-dom": "Provides support for XSD validation for XML mapping files", @@ -1238,7 +1361,7 @@ "issues": "https://github.com/doctrine/orm/issues", "source": "https://github.com/doctrine/orm/tree/3.4.x" }, - "time": "2024-11-23T21:01:13+00:00" + "time": "2024-12-08T12:02:05+00:00" }, { "name": "doctrine/persistence", @@ -1343,12 +1466,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "b4068e8d1a5168769ce65410bab76a8362e04da0" + "reference": "579b67954ca6817a4d5a0a785c654fb7bc849b2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/b4068e8d1a5168769ce65410bab76a8362e04da0", - "reference": "b4068e8d1a5168769ce65410bab76a8362e04da0", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/579b67954ca6817a4d5a0a785c654fb7bc849b2e", + "reference": "579b67954ca6817a4d5a0a785c654fb7bc849b2e", "shasum": "" }, "require": { @@ -1358,8 +1481,7 @@ "doctrine/coding-standard": "^12", "ergebnis/phpunit-slow-test-detector": "^2.14", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.24" + "phpunit/phpunit": "^10.5" }, "default-branch": true, "bin": [ @@ -1392,7 +1514,7 @@ "issues": "https://github.com/doctrine/sql-formatter/issues", "source": "https://github.com/doctrine/sql-formatter/tree/1.5.x" }, - "time": "2024-11-25T11:48:05+00:00" + "time": "2024-12-02T22:10:47+00:00" }, { "name": "easycorp/easyadmin-bundle", @@ -1400,12 +1522,12 @@ "source": { "type": "git", "url": "https://github.com/EasyCorp/EasyAdminBundle.git", - "reference": "2307da58ccfd0569f65943b9a01d49a4f3a71d7d" + "reference": "8d6b02d39da311f6ca1e56a2ec5328062bcba891" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/EasyCorp/EasyAdminBundle/zipball/2307da58ccfd0569f65943b9a01d49a4f3a71d7d", - "reference": "2307da58ccfd0569f65943b9a01d49a4f3a71d7d", + "url": "https://api.github.com/repos/EasyCorp/EasyAdminBundle/zipball/8d6b02d39da311f6ca1e56a2ec5328062bcba891", + "reference": "8d6b02d39da311f6ca1e56a2ec5328062bcba891", "shasum": "" }, "require": { @@ -1433,7 +1555,8 @@ "symfony/twig-bundle": "^5.4|^6.0|^7.0", "symfony/uid": "^5.4|^6.0|^7.0", "symfony/ux-twig-component": "^2.21", - "symfony/validator": "^5.4|^6.0|^7.0" + "symfony/validator": "^5.4|^6.0|^7.0", + "twig/twig": "^3.15" }, "require-dev": { "doctrine/doctrine-fixtures-bundle": "^3.4|3.5.x-dev", @@ -1448,7 +1571,8 @@ "symfony/debug-bundle": "^5.4|^6.0|^7.0", "symfony/dom-crawler": "^5.4|^6.0|^7.0", "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/phpunit-bridge": "^6.1|^7.0" + "symfony/phpunit-bridge": "^6.1|^7.0", + "symfony/process": "^5.4|^6.0|^7.0" }, "default-branch": true, "type": "symfony-bundle", @@ -1489,7 +1613,7 @@ "type": "github" } ], - "time": "2024-11-29T18:27:19+00:00" + "time": "2024-12-07T16:15:10+00:00" }, { "name": "egulias/email-validator", @@ -1637,12 +1761,12 @@ "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "bbef3301fda0b97bbd8d7a897800424fb5937c9e" + "reference": "b561666ac77bfbde7f0cc9bc4091e0db56b9f573" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/bbef3301fda0b97bbd8d7a897800424fb5937c9e", - "reference": "bbef3301fda0b97bbd8d7a897800424fb5937c9e", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/b561666ac77bfbde7f0cc9bc4091e0db56b9f573", + "reference": "b561666ac77bfbde7f0cc9bc4091e0db56b9f573", "shasum": "" }, "require": { @@ -1667,8 +1791,9 @@ "phpstan/phpstan": "^1.8.2", "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0 || ^7.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0", + "symfony/finder": "^5.3 | ^6.0 | ^7.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", "unleashedtech/php-coding-standard": "^3.1.1", "vimeo/psalm": "^4.24.0 || ^5.0.0" }, @@ -1678,7 +1803,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.6-dev" + "dev-main": "2.7-dev" } }, "autoload": { @@ -1735,7 +1860,7 @@ "type": "tidelift" } ], - "time": "2024-09-08T13:06:11+00:00" + "time": "2024-12-07T15:56:47+00:00" }, { "name": "league/config", @@ -1893,12 +2018,12 @@ "source": { "type": "git", "url": "https://github.com/meilisearch/meilisearch-symfony.git", - "reference": "d46b2da1865ef228aaca67eeac9e6851022dbeaf" + "reference": "345bb2b18ec0a962a8a12d0ae0e8088c2ebf0641" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/meilisearch/meilisearch-symfony/zipball/d46b2da1865ef228aaca67eeac9e6851022dbeaf", - "reference": "d46b2da1865ef228aaca67eeac9e6851022dbeaf", + "url": "https://api.github.com/repos/meilisearch/meilisearch-symfony/zipball/345bb2b18ec0a962a8a12d0ae0e8088c2ebf0641", + "reference": "345bb2b18ec0a962a8a12d0ae0e8088c2ebf0641", "shasum": "" }, "require": { @@ -1963,9 +2088,9 @@ ], "support": { "issues": "https://github.com/meilisearch/meilisearch-symfony/issues", - "source": "https://github.com/meilisearch/meilisearch-symfony/tree/main" + "source": "https://github.com/meilisearch/meilisearch-symfony/tree/v0.15.7" }, - "time": "2024-12-01T18:32:28+00:00" + "time": "2024-12-03T12:49:45+00:00" }, { "name": "monolog/monolog", @@ -1973,12 +2098,12 @@ "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "e94000419394ff1bec801dad310432228f9fc19c" + "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/e94000419394ff1bec801dad310432228f9fc19c", - "reference": "e94000419394ff1bec801dad310432228f9fc19c", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/aef6ee73a77a66e404dd6540934a9ef1b3c855b4", + "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4", "shasum": "" }, "require": { @@ -2057,7 +2182,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/main" + "source": "https://github.com/Seldaek/monolog/tree/3.8.1" }, "funding": [ { @@ -2069,7 +2194,7 @@ "type": "tidelift" } ], - "time": "2024-11-17T12:30:33+00:00" + "time": "2024-12-05T17:15:07+00:00" }, { "name": "nette/schema", @@ -2220,6 +2345,60 @@ }, "time": "2024-08-07T15:39:19+00:00" }, + { + "name": "notfloran/mjml-bundle", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/Akollade/mjml-bundle.git", + "reference": "651865d5aac928b0583cb2bb52959d9debad6e1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Akollade/mjml-bundle/zipball/651865d5aac928b0583cb2bb52959d9debad6e1e", + "reference": "651865d5aac928b0583cb2bb52959d9debad6e1e", + "shasum": "" + }, + "require": { + "php": ">=7.1.0", + "symfony/dependency-injection": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "symfony/process": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "symfony/twig-bundle": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "require-dev": { + "matthiasnoback/symfony-config-test": "^4.1", + "phpstan/phpstan": "^0.12.0", + "phpunit/phpunit": "^7.0|^8.0", + "symplify/easy-coding-standard": "^9.0" + }, + "default-branch": true, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "NotFloran\\MjmlBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Floran Brutel", + "email": "fbrutel@akollade.fr" + } + ], + "description": "Symfony bundle for MJML", + "keywords": [ + "email", + "mjml" + ], + "support": { + "issues": "https://github.com/Akollade/mjml-bundle/issues", + "source": "https://github.com/Akollade/mjml-bundle/tree/v3.8.3" + }, + "time": "2024-12-09T14:15:48+00:00" + }, { "name": "nyholm/psr7", "version": "dev-master", @@ -2650,12 +2829,12 @@ "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "f3558a4c23426d12bffeaab463f8a8d8b681193c" + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/f3558a4c23426d12bffeaab463f8a8d8b681193c", - "reference": "f3558a4c23426d12bffeaab463f8a8d8b681193c", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", "shasum": "" }, "require": { @@ -2705,9 +2884,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.0" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.1" }, - "time": "2024-11-12T11:25:25+00:00" + "time": "2024-12-07T09:39:29+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -3300,12 +3479,12 @@ "source": { "type": "git", "url": "https://github.com/php-runtime/frankenphp-symfony.git", - "reference": "38a5dfa1b1e40d8e0b3bbc91d84a03cf4e65fcf4" + "reference": "34b8c25e4b1043dec2a51dfebbc776260acf1921" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-runtime/frankenphp-symfony/zipball/38a5dfa1b1e40d8e0b3bbc91d84a03cf4e65fcf4", - "reference": "38a5dfa1b1e40d8e0b3bbc91d84a03cf4e65fcf4", + "url": "https://api.github.com/repos/php-runtime/frankenphp-symfony/zipball/34b8c25e4b1043dec2a51dfebbc776260acf1921", + "reference": "34b8c25e4b1043dec2a51dfebbc776260acf1921", "shasum": "" }, "require": { @@ -3345,7 +3524,7 @@ "type": "github" } ], - "time": "2024-06-14T20:56:26+00:00" + "time": "2024-12-03T16:13:06+00:00" }, { "name": "sensiolabs/typescript-bundle", @@ -3404,6 +3583,72 @@ }, "time": "2024-09-17T12:41:18+00:00" }, + { + "name": "symfony/amazon-mailer", + "version": "7.3.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/amazon-mailer.git", + "reference": "7dd225078938bc52b114cd295449170e25ca9229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/amazon-mailer/zipball/7dd225078938bc52b114cd295449170e25ca9229", + "reference": "7dd225078938bc52b114cd295449170e25ca9229", + "shasum": "" + }, + "require": { + "async-aws/ses": "^1.3", + "php": ">=8.2", + "symfony/mailer": "^7.2" + }, + "require-dev": { + "symfony/http-client": "^6.4|^7.0" + }, + "type": "symfony-mailer-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\Bridge\\Amazon\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Amazon Mailer Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/amazon-mailer/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-28T08:24:38+00:00" + }, { "name": "symfony/asset", "version": "7.3.x-dev", @@ -3479,12 +3724,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/asset-mapper.git", - "reference": "ffb733232bb6bb85ef6a994f47c817e7c2ecab9c" + "reference": "777908c1b580a51b1452ab95dc90ef9896572fa5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/asset-mapper/zipball/ffb733232bb6bb85ef6a994f47c817e7c2ecab9c", - "reference": "ffb733232bb6bb85ef6a994f47c817e7c2ecab9c", + "url": "https://api.github.com/repos/symfony/asset-mapper/zipball/777908c1b580a51b1452ab95dc90ef9896572fa5", + "reference": "777908c1b580a51b1452ab95dc90ef9896572fa5", "shasum": "" }, "require": { @@ -3506,6 +3751,7 @@ "symfony/framework-bundle": "^6.4|^7.0", "symfony/http-foundation": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", "symfony/web-link": "^6.4|^7.0" }, "type": "library", @@ -3534,7 +3780,7 @@ "description": "Maps directories of assets & makes them available in a public directory with versioned filenames.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/asset-mapper/tree/v7.2.0" + "source": "https://github.com/symfony/asset-mapper/tree/7.3" }, "funding": [ { @@ -3550,7 +3796,7 @@ "type": "tidelift" } ], - "time": "2024-11-20T11:17:29+00:00" + "time": "2024-12-07T15:27:34+00:00" }, { "name": "symfony/cache", @@ -3558,12 +3804,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "2c926bc348184b4b235f2200fcbe8fcf3c8c5b8a" + "reference": "e7e983596b744c4539f31e79b0350a6cf5878a20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/2c926bc348184b4b235f2200fcbe8fcf3c8c5b8a", - "reference": "2c926bc348184b4b235f2200fcbe8fcf3c8c5b8a", + "url": "https://api.github.com/repos/symfony/cache/zipball/e7e983596b744c4539f31e79b0350a6cf5878a20", + "reference": "e7e983596b744c4539f31e79b0350a6cf5878a20", "shasum": "" }, "require": { @@ -3632,7 +3878,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v7.2.0" + "source": "https://github.com/symfony/cache/tree/7.2" }, "funding": [ { @@ -3648,7 +3894,7 @@ "type": "tidelift" } ], - "time": "2024-11-25T15:21:05+00:00" + "time": "2024-12-07T08:08:50+00:00" }, { "name": "symfony/cache-contracts", @@ -3882,12 +4128,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "23c8aae6d764e2bae02d2a99f7532a7f6ed619cf" + "reference": "722fa7cd85d81a3e61fcbdce053754c6541235d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/23c8aae6d764e2bae02d2a99f7532a7f6ed619cf", - "reference": "23c8aae6d764e2bae02d2a99f7532a7f6ed619cf", + "url": "https://api.github.com/repos/symfony/console/zipball/722fa7cd85d81a3e61fcbdce053754c6541235d3", + "reference": "722fa7cd85d81a3e61fcbdce053754c6541235d3", "shasum": "" }, "require": { @@ -3951,7 +4197,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.0" + "source": "https://github.com/symfony/console/tree/7.2" }, "funding": [ { @@ -3967,7 +4213,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2024-12-09T09:56:03+00:00" }, { "name": "symfony/dependency-injection", @@ -4123,12 +4369,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "09dbb7c731430335e9ae89ee5054b5f5580c49bf" + "reference": "b492be51eb703723d682851a0c9fb39b9d1a7bfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/09dbb7c731430335e9ae89ee5054b5f5580c49bf", - "reference": "09dbb7c731430335e9ae89ee5054b5f5580c49bf", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/b492be51eb703723d682851a0c9fb39b9d1a7bfb", + "reference": "b492be51eb703723d682851a0c9fb39b9d1a7bfb", "shasum": "" }, "require": { @@ -4208,7 +4454,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v7.2.0" + "source": "https://github.com/symfony/doctrine-bridge/tree/7.2" }, "funding": [ { @@ -4224,7 +4470,7 @@ "type": "tidelift" } ], - "time": "2024-11-25T12:10:02+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/doctrine-messenger", @@ -4378,12 +4624,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "672b3dd1ef8b87119b446d67c58c106c43f965fe" + "reference": "422c8d8d7ee1e2b8d871de434b4f706982f0f029" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/672b3dd1ef8b87119b446d67c58c106c43f965fe", - "reference": "672b3dd1ef8b87119b446d67c58c106c43f965fe", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/422c8d8d7ee1e2b8d871de434b4f706982f0f029", + "reference": "422c8d8d7ee1e2b8d871de434b4f706982f0f029", "shasum": "" }, "require": { @@ -4429,7 +4675,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.2.0" + "source": "https://github.com/symfony/error-handler/tree/7.3" }, "funding": [ { @@ -4445,7 +4691,7 @@ "type": "tidelift" } ], - "time": "2024-11-05T15:35:02+00:00" + "time": "2024-12-07T11:46:47+00:00" }, { "name": "symfony/event-dispatcher", @@ -4906,12 +5152,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "a8d0da4110fe643ab3cde7c938a03e222fe787c6" + "reference": "af6c33e856f26f21cd236b89df5884b45cc782d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/a8d0da4110fe643ab3cde7c938a03e222fe787c6", - "reference": "a8d0da4110fe643ab3cde7c938a03e222fe787c6", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/af6c33e856f26f21cd236b89df5884b45cc782d3", + "reference": "af6c33e856f26f21cd236b89df5884b45cc782d3", "shasum": "" }, "require": { @@ -5032,7 +5278,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v7.2.0" + "source": "https://github.com/symfony/framework-bundle/tree/7.3" }, "funding": [ { @@ -5048,7 +5294,7 @@ "type": "tidelift" } ], - "time": "2024-11-20T16:27:35+00:00" + "time": "2024-12-09T10:05:03+00:00" }, { "name": "symfony/http-client", @@ -5056,19 +5302,19 @@ "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "955e43336aff03df1e8a8e17daefabb0127a313b" + "reference": "ff4df2b68d1c67abb9fef146e6540ea16b58d99e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/955e43336aff03df1e8a8e17daefabb0127a313b", - "reference": "955e43336aff03df1e8a8e17daefabb0127a313b", + "url": "https://api.github.com/repos/symfony/http-client/zipball/ff4df2b68d1c67abb9fef146e6540ea16b58d99e", + "reference": "ff4df2b68d1c67abb9fef146e6540ea16b58d99e", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "~3.4.3|^3.5.1", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -5143,7 +5389,7 @@ "type": "tidelift" } ], - "time": "2024-11-29T08:22:02+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/http-client-contracts", @@ -5151,12 +5397,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "7917bba9674e386bc2726c4bb9ad5440f7831d66" + "reference": "c5d993f8c4ed8c1773ce8c0e92de39a90fae6ac3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/7917bba9674e386bc2726c4bb9ad5440f7831d66", - "reference": "7917bba9674e386bc2726c4bb9ad5440f7831d66", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/c5d993f8c4ed8c1773ce8c0e92de39a90fae6ac3", + "reference": "c5d993f8c4ed8c1773ce8c0e92de39a90fae6ac3", "shasum": "" }, "require": { @@ -5165,12 +5411,12 @@ "default-branch": true, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.6-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -5222,7 +5468,7 @@ "type": "tidelift" } ], - "time": "2024-11-19T10:11:42+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/http-foundation", @@ -5308,12 +5554,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "52b0f33f21683a1a78a61a762a7632f45942f97d" + "reference": "1078dfea48501bcf5f5e1fdc59b273a2c130f3e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/52b0f33f21683a1a78a61a762a7632f45942f97d", - "reference": "52b0f33f21683a1a78a61a762a7632f45942f97d", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/1078dfea48501bcf5f5e1fdc59b273a2c130f3e5", + "reference": "1078dfea48501bcf5f5e1fdc59b273a2c130f3e5", "shasum": "" }, "require": { @@ -5414,7 +5660,7 @@ "type": "tidelift" } ], - "time": "2024-11-20T12:24:43+00:00" + "time": "2024-12-07T08:55:22+00:00" }, { "name": "symfony/intl", @@ -5737,12 +5983,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/messenger.git", - "reference": "2512b9bc1e7093c8bd5adec579a364a198059f4d" + "reference": "cc0e820c02a0a887a88ddb52b7c4de4634677ce6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/messenger/zipball/2512b9bc1e7093c8bd5adec579a364a198059f4d", - "reference": "2512b9bc1e7093c8bd5adec579a364a198059f4d", + "url": "https://api.github.com/repos/symfony/messenger/zipball/cc0e820c02a0a887a88ddb52b7c4de4634677ce6", + "reference": "cc0e820c02a0a887a88ddb52b7c4de4634677ce6", "shasum": "" }, "require": { @@ -5800,7 +6046,7 @@ "description": "Helps applications send and receive messages to/from other applications or via message queues", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/messenger/tree/v7.2.0" + "source": "https://github.com/symfony/messenger/tree/7.2" }, "funding": [ { @@ -5816,7 +6062,7 @@ "type": "tidelift" } ], - "time": "2024-11-26T10:00:31+00:00" + "time": "2024-12-07T08:08:50+00:00" }, { "name": "symfony/mime", @@ -5824,12 +6070,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "cc84a4b81f62158c3846ac7ff10f696aae2b524d" + "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/cc84a4b81f62158c3846ac7ff10f696aae2b524d", - "reference": "cc84a4b81f62158c3846ac7ff10f696aae2b524d", + "url": "https://api.github.com/repos/symfony/mime/zipball/7f9617fcf15cb61be30f8b252695ed5e2bfac283", + "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283", "shasum": "" }, "require": { @@ -5884,7 +6130,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.0" + "source": "https://github.com/symfony/mime/tree/7.2" }, "funding": [ { @@ -5900,7 +6146,7 @@ "type": "tidelift" } ], - "time": "2024-11-23T09:19:39+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/monolog-bridge", @@ -6303,8 +6549,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -6551,8 +6797,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -6717,8 +6963,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -6913,12 +7159,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "b00580d9d7c9654e1df95df85105d0da67418b3f" + "reference": "65fb9be15380f949d72ff405473cce733364b8b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/b00580d9d7c9654e1df95df85105d0da67418b3f", - "reference": "b00580d9d7c9654e1df95df85105d0da67418b3f", + "url": "https://api.github.com/repos/symfony/property-info/zipball/65fb9be15380f949d72ff405473cce733364b8b4", + "reference": "65fb9be15380f949d72ff405473cce733364b8b4", "shasum": "" }, "require": { @@ -6972,7 +7218,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v7.2.0" + "source": "https://github.com/symfony/property-info/tree/7.2" }, "funding": [ { @@ -6988,7 +7234,74 @@ "type": "tidelift" } ], - "time": "2024-11-27T09:50:52+00:00" + "time": "2024-12-07T08:50:44+00:00" + }, + { + "name": "symfony/redis-messenger", + "version": "7.3.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/redis-messenger.git", + "reference": "3895c75030984ed99945d2cb89158f11a0e3c4e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/redis-messenger/zipball/3895c75030984ed99945d2cb89158f11a0e3c4e8", + "reference": "3895c75030984ed99945d2cb89158f11a0e3c4e8", + "shasum": "" + }, + "require": { + "ext-redis": "*", + "php": ">=8.2", + "symfony/messenger": "^6.4|^7.0" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0" + }, + "type": "symfony-messenger-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\Bridge\\Redis\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Redis extension Messenger Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/redis-messenger/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-26T10:00:31+00:00" }, { "name": "symfony/routing", @@ -7156,12 +7469,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "4bed2029576bf02a6915c5a58bc8a174af338e6f" + "reference": "626c686874aaad93d6460c976b49ff3826ba6e93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/4bed2029576bf02a6915c5a58bc8a174af338e6f", - "reference": "4bed2029576bf02a6915c5a58bc8a174af338e6f", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/626c686874aaad93d6460c976b49ff3826ba6e93", + "reference": "626c686874aaad93d6460c976b49ff3826ba6e93", "shasum": "" }, "require": { @@ -7175,7 +7488,7 @@ "symfony/http-foundation": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/password-hasher": "^6.4|^7.0", - "symfony/security-core": "^7.2", + "symfony/security-core": "^7.3", "symfony/security-csrf": "^6.4|^7.0", "symfony/security-http": "^7.2", "symfony/service-contracts": "^2.5|^3" @@ -7238,7 +7551,7 @@ "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-bundle/tree/v7.2.0" + "source": "https://github.com/symfony/security-bundle/tree/7.3" }, "funding": [ { @@ -7254,7 +7567,7 @@ "type": "tidelift" } ], - "time": "2024-10-23T08:31:32+00:00" + "time": "2024-12-07T21:01:37+00:00" }, { "name": "symfony/security-core", @@ -7262,12 +7575,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/security-core.git", - "reference": "fdbf318b939a86f89b0c071f60b9d551261d3cc1" + "reference": "312a726ac8b92bf1544355eefd2d290211d3119b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-core/zipball/fdbf318b939a86f89b0c071f60b9d551261d3cc1", - "reference": "fdbf318b939a86f89b0c071f60b9d551261d3cc1", + "url": "https://api.github.com/repos/symfony/security-core/zipball/312a726ac8b92bf1544355eefd2d290211d3119b", + "reference": "312a726ac8b92bf1544355eefd2d290211d3119b", "shasum": "" }, "require": { @@ -7325,7 +7638,7 @@ "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-core/tree/v7.2.0" + "source": "https://github.com/symfony/security-core/tree/7.3" }, "funding": [ { @@ -7341,7 +7654,7 @@ "type": "tidelift" } ], - "time": "2024-11-27T09:50:52+00:00" + "time": "2024-12-07T13:40:54+00:00" }, { "name": "symfony/security-csrf", @@ -7419,12 +7732,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "0d0ab4d491f22306c893b2d30ce73ea911201a61" + "reference": "125844598d9cef4fe72a9f6c4a78ac7c59c3f532" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/0d0ab4d491f22306c893b2d30ce73ea911201a61", - "reference": "0d0ab4d491f22306c893b2d30ce73ea911201a61", + "url": "https://api.github.com/repos/symfony/security-http/zipball/125844598d9cef4fe72a9f6c4a78ac7c59c3f532", + "reference": "125844598d9cef4fe72a9f6c4a78ac7c59c3f532", "shasum": "" }, "require": { @@ -7483,7 +7796,7 @@ "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-http/tree/v7.2.0" + "source": "https://github.com/symfony/security-http/tree/7.2" }, "funding": [ { @@ -7499,7 +7812,7 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:40:36+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/serializer", @@ -7689,12 +8002,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/stimulus-bundle.git", - "reference": "2e840a3b12f06b33441cc3eb8907f51b806a7e4b" + "reference": "e13034d428354023c82a1db108d40fdf6cec2d36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/2e840a3b12f06b33441cc3eb8907f51b806a7e4b", - "reference": "2e840a3b12f06b33441cc3eb8907f51b806a7e4b", + "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/e13034d428354023c82a1db108d40fdf6cec2d36", + "reference": "e13034d428354023c82a1db108d40fdf6cec2d36", "shasum": "" }, "require": { @@ -7735,7 +8048,7 @@ "symfony-ux" ], "support": { - "source": "https://github.com/symfony/stimulus-bundle/tree/v2.22.0" + "source": "https://github.com/symfony/stimulus-bundle/tree/2.x" }, "funding": [ { @@ -7751,7 +8064,7 @@ "type": "tidelift" } ], - "time": "2024-11-20T07:57:38+00:00" + "time": "2024-12-06T14:30:33+00:00" }, { "name": "symfony/stopwatch", @@ -8082,12 +8395,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "1343696b988e72beafc63a6cf386922ccb314a08" + "reference": "d5cdf4d59da5ab44ebd7503480c22d8235887de0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/1343696b988e72beafc63a6cf386922ccb314a08", - "reference": "1343696b988e72beafc63a6cf386922ccb314a08", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/d5cdf4d59da5ab44ebd7503480c22d8235887de0", + "reference": "d5cdf4d59da5ab44ebd7503480c22d8235887de0", "shasum": "" }, "require": { @@ -8184,7 +8497,7 @@ "type": "tidelift" } ], - "time": "2024-12-02T12:43:01+00:00" + "time": "2024-12-07T09:50:32+00:00" }, { "name": "symfony/twig-bundle", @@ -8276,12 +8589,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "e0bfd95bceb3886c59487828537691aecb7d9c6b" + "reference": "cf153a6172679757551365a8762a06ab6603c714" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/e0bfd95bceb3886c59487828537691aecb7d9c6b", - "reference": "e0bfd95bceb3886c59487828537691aecb7d9c6b", + "url": "https://api.github.com/repos/symfony/type-info/zipball/cf153a6172679757551365a8762a06ab6603c714", + "reference": "cf153a6172679757551365a8762a06ab6603c714", "shasum": "" }, "require": { @@ -8332,7 +8645,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.2.0" + "source": "https://github.com/symfony/type-info/tree/7.2" }, "funding": [ { @@ -8348,7 +8661,7 @@ "type": "tidelift" } ], - "time": "2024-11-18T09:51:31+00:00" + "time": "2024-12-07T08:08:50+00:00" }, { "name": "symfony/uid", @@ -8356,12 +8669,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "2d294d0c48df244c71c105a169d0190bfb080426" + "reference": "f5e643aee01ab83011b08a466b5194131763dd3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/2d294d0c48df244c71c105a169d0190bfb080426", - "reference": "2d294d0c48df244c71c105a169d0190bfb080426", + "url": "https://api.github.com/repos/symfony/uid/zipball/f5e643aee01ab83011b08a466b5194131763dd3e", + "reference": "f5e643aee01ab83011b08a466b5194131763dd3e", "shasum": "" }, "require": { @@ -8406,7 +8719,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.2.0" + "source": "https://github.com/symfony/uid/tree/7.3" }, "funding": [ { @@ -8422,7 +8735,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2024-12-07T08:11:28+00:00" }, { "name": "symfony/ux-chartjs", @@ -8430,12 +8743,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/ux-chartjs.git", - "reference": "32476b05eb1bd76dc049a2747cf398e76a9a44a5" + "reference": "6e7de01ea469840da2b7458b660b52e846e279e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-chartjs/zipball/32476b05eb1bd76dc049a2747cf398e76a9a44a5", - "reference": "32476b05eb1bd76dc049a2747cf398e76a9a44a5", + "url": "https://api.github.com/repos/symfony/ux-chartjs/zipball/6e7de01ea469840da2b7458b660b52e846e279e1", + "reference": "6e7de01ea469840da2b7458b660b52e846e279e1", "shasum": "" }, "require": { @@ -8487,7 +8800,7 @@ "symfony-ux" ], "support": { - "source": "https://github.com/symfony/ux-chartjs/tree/v2.22.0" + "source": "https://github.com/symfony/ux-chartjs/tree/2.x" }, "funding": [ { @@ -8503,7 +8816,7 @@ "type": "tidelift" } ], - "time": "2024-11-20T07:57:38+00:00" + "time": "2024-12-05T14:25:02+00:00" }, { "name": "symfony/ux-live-component", @@ -8511,12 +8824,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/ux-live-component.git", - "reference": "2df6a25f25788864e65cb8812d85e14ef80b6b44" + "reference": "060e0c64e64125a4dfbf37dec281157faade1feb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-live-component/zipball/2df6a25f25788864e65cb8812d85e14ef80b6b44", - "reference": "2df6a25f25788864e65cb8812d85e14ef80b6b44", + "url": "https://api.github.com/repos/symfony/ux-live-component/zipball/060e0c64e64125a4dfbf37dec281157faade1feb", + "reference": "060e0c64e64125a4dfbf37dec281157faade1feb", "shasum": "" }, "require": { @@ -8582,7 +8895,7 @@ "twig" ], "support": { - "source": "https://github.com/symfony/ux-live-component/tree/v2.22.0" + "source": "https://github.com/symfony/ux-live-component/tree/2.x" }, "funding": [ { @@ -8598,7 +8911,7 @@ "type": "tidelift" } ], - "time": "2024-11-29T15:31:04+00:00" + "time": "2024-12-07T10:13:15+00:00" }, { "name": "symfony/ux-turbo", @@ -8606,12 +8919,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/ux-turbo.git", - "reference": "f7af0aa09190354dd4630ea330d8a3fc3e8ef278" + "reference": "97718ea4bca26f0db843c3c0de338d6900c5a002" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/f7af0aa09190354dd4630ea330d8a3fc3e8ef278", - "reference": "f7af0aa09190354dd4630ea330d8a3fc3e8ef278", + "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/97718ea4bca26f0db843c3c0de338d6900c5a002", + "reference": "97718ea4bca26f0db843c3c0de338d6900c5a002", "shasum": "" }, "require": { @@ -8681,7 +8994,7 @@ "turbo-stream" ], "support": { - "source": "https://github.com/symfony/ux-turbo/tree/v2.22.0" + "source": "https://github.com/symfony/ux-turbo/tree/2.x" }, "funding": [ { @@ -8697,7 +9010,7 @@ "type": "tidelift" } ], - "time": "2024-11-29T15:25:16+00:00" + "time": "2024-12-05T14:25:02+00:00" }, { "name": "symfony/ux-twig-component", @@ -8705,12 +9018,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/ux-twig-component.git", - "reference": "03177a494399fbdcbb1f5f2aee017ccf8df581d9" + "reference": "9b347f6ca2d9e18cee630787f0a6aa453982bf18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/03177a494399fbdcbb1f5f2aee017ccf8df581d9", - "reference": "03177a494399fbdcbb1f5f2aee017ccf8df581d9", + "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/9b347f6ca2d9e18cee630787f0a6aa453982bf18", + "reference": "9b347f6ca2d9e18cee630787f0a6aa453982bf18", "shasum": "" }, "require": { @@ -8765,7 +9078,7 @@ "twig" ], "support": { - "source": "https://github.com/symfony/ux-twig-component/tree/v2.22.0" + "source": "https://github.com/symfony/ux-twig-component/tree/2.x" }, "funding": [ { @@ -8781,7 +9094,7 @@ "type": "tidelift" } ], - "time": "2024-11-23T06:59:34+00:00" + "time": "2024-12-07T18:05:50+00:00" }, { "name": "symfony/validator", @@ -8886,12 +9199,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "340357866b7ebea88d2f325c167cb5e4dd3383e4" + "reference": "de6124d690069ee8d4cd21b00050aa231c0434e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/340357866b7ebea88d2f325c167cb5e4dd3383e4", - "reference": "340357866b7ebea88d2f325c167cb5e4dd3383e4", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/de6124d690069ee8d4cd21b00050aa231c0434e3", + "reference": "de6124d690069ee8d4cd21b00050aa231c0434e3", "shasum": "" }, "require": { @@ -8961,7 +9274,7 @@ "type": "tidelift" } ], - "time": "2024-11-28T14:07:15+00:00" + "time": "2024-11-28T16:26:37+00:00" }, { "name": "symfony/var-exporter", @@ -9256,12 +9569,12 @@ "source": { "type": "git", "url": "https://github.com/twbs/bootstrap.git", - "reference": "ec96eacd0e6f297a64ee058b22ce9f567c0860e3" + "reference": "760b4cc41dcf09ad7f68c6b3b11768755ff2ec0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twbs/bootstrap/zipball/ec96eacd0e6f297a64ee058b22ce9f567c0860e3", - "reference": "ec96eacd0e6f297a64ee058b22ce9f567c0860e3", + "url": "https://api.github.com/repos/twbs/bootstrap/zipball/760b4cc41dcf09ad7f68c6b3b11768755ff2ec0b", + "reference": "760b4cc41dcf09ad7f68c6b3b11768755ff2ec0b", "shasum": "" }, "replace": { @@ -9299,7 +9612,7 @@ "issues": "https://github.com/twbs/bootstrap/issues", "source": "https://github.com/twbs/bootstrap/tree/main" }, - "time": "2024-11-22T09:54:10+00:00" + "time": "2024-12-07T05:31:01+00:00" }, { "name": "twig/extra-bundle", @@ -9984,12 +10297,12 @@ "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "23534bff9eb778be9b1c55570548492741c3393c" + "reference": "0fa6fe639a961a0765d4bc8fb341731cf1c4e03f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/23534bff9eb778be9b1c55570548492741c3393c", - "reference": "23534bff9eb778be9b1c55570548492741c3393c", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/0fa6fe639a961a0765d4bc8fb341731cf1c4e03f", + "reference": "0fa6fe639a961a0765d4bc8fb341731cf1c4e03f", "shasum": "" }, "require": { @@ -10080,7 +10393,7 @@ "type": "github" } ], - "time": "2024-11-30T08:32:38+00:00" + "time": "2024-12-04T01:12:42+00:00" }, { "name": "masterminds/html5", @@ -10442,12 +10755,12 @@ "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "f8d27d5b81b23d9b679ca2ccac09261c461a15f4" + "reference": "89ddae6cfbfbce545dd694caecfaa3872652cff2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f8d27d5b81b23d9b679ca2ccac09261c461a15f4", - "reference": "f8d27d5b81b23d9b679ca2ccac09261c461a15f4", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/89ddae6cfbfbce545dd694caecfaa3872652cff2", + "reference": "89ddae6cfbfbce545dd694caecfaa3872652cff2", "shasum": "" }, "require": { @@ -10493,7 +10806,7 @@ "type": "github" } ], - "time": "2024-12-02T07:51:15+00:00" + "time": "2024-12-09T09:17:33+00:00" }, { "name": "phpstan/phpstan-doctrine", @@ -10746,12 +11059,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "5a5c19843209812d6c8cb91d30478dfce3d8bdad" + "reference": "e656040b5bdab9d6ca046287327bbb47c62be886" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5a5c19843209812d6c8cb91d30478dfce3d8bdad", - "reference": "5a5c19843209812d6c8cb91d30478dfce3d8bdad", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e656040b5bdab9d6ca046287327bbb47c62be886", + "reference": "e656040b5bdab9d6ca046287327bbb47c62be886", "shasum": "" }, "require": { @@ -10816,7 +11129,7 @@ "type": "github" } ], - "time": "2024-10-31T05:58:31+00:00" + "time": "2024-12-07T06:07:48+00:00" }, { "name": "phpunit/php-file-iterator", @@ -11069,12 +11382,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e75fd04dce45b3a064a44107d8037b411ecbd307" + "reference": "a770e0df716b20e92580f0f5ca9d4f7c948106fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e75fd04dce45b3a064a44107d8037b411ecbd307", - "reference": "e75fd04dce45b3a064a44107d8037b411ecbd307", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a770e0df716b20e92580f0f5ca9d4f7c948106fd", + "reference": "a770e0df716b20e92580f0f5ca9d4f7c948106fd", "shasum": "" }, "require": { @@ -11162,7 +11475,7 @@ "type": "tidelift" } ], - "time": "2024-11-30T07:38:36+00:00" + "time": "2024-12-07T13:58:59+00:00" }, { "name": "react/cache", @@ -13207,12 +13520,12 @@ "source": { "type": "git", "url": "https://github.com/VincentLanglet/Twig-CS-Fixer.git", - "reference": "0c50bdb80b2de1a39e6d3d3ad96255a0ebdbeab0" + "reference": "a193004602d7a9b1c17408eb8b8a1632532a28a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/VincentLanglet/Twig-CS-Fixer/zipball/0c50bdb80b2de1a39e6d3d3ad96255a0ebdbeab0", - "reference": "0c50bdb80b2de1a39e6d3d3ad96255a0ebdbeab0", + "url": "https://api.github.com/repos/VincentLanglet/Twig-CS-Fixer/zipball/a193004602d7a9b1c17408eb8b8a1632532a28a7", + "reference": "a193004602d7a9b1c17408eb8b8a1632532a28a7", "shasum": "" }, "require": { @@ -13272,7 +13585,7 @@ "homepage": "https://github.com/VincentLanglet/Twig-CS-Fixer", "support": { "issues": "https://github.com/VincentLanglet/Twig-CS-Fixer/issues", - "source": "https://github.com/VincentLanglet/Twig-CS-Fixer/tree/main" + "source": "https://github.com/VincentLanglet/Twig-CS-Fixer/tree/3.4.0" }, "funding": [ { @@ -13280,7 +13593,7 @@ "type": "github" } ], - "time": "2024-11-30T08:25:10+00:00" + "time": "2024-12-04T18:37:59+00:00" } ], "aliases": [], @@ -13290,6 +13603,7 @@ "jblond/php-diff": 20, "league/commonmark": 20, "meilisearch/search-bundle": 20, + "notfloran/mjml-bundle": 20, "nyholm/psr7": 20, "openai-php/client": 20, "oro/doctrine-extensions": 20, diff --git a/config/bundles.php b/config/bundles.php index 57e466f..d517e16 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -22,4 +22,5 @@ Sensiolabs\TypeScriptBundle\SensiolabsTypeScriptBundle::class => ['all' => true], Meilisearch\Bundle\MeilisearchBundle::class => ['all' => true], Symfony\UX\Chartjs\ChartjsBundle::class => ['all' => true], + NotFloran\MjmlBundle\MjmlBundle::class => ['all' => true], ]; diff --git a/config/packages/asset_mapper.php b/config/packages/asset_mapper.php new file mode 100644 index 0000000..04de67d --- /dev/null +++ b/config/packages/asset_mapper.php @@ -0,0 +1,15 @@ +assetMapper() + ->path('assets/', '') + ->path('vendor/twbs/bootstrap/', '') + ->excludedPatterns([ + '*/assets/styles/_*.scss', + '*/assets/styles/**/_*.scss', + ]); +}; diff --git a/config/packages/asset_mapper.yaml b/config/packages/asset_mapper.yaml deleted file mode 100644 index 03dc576..0000000 --- a/config/packages/asset_mapper.yaml +++ /dev/null @@ -1,13 +0,0 @@ -framework: - asset_mapper: - # The paths to make available to the asset mapper. - paths: - - assets/ - - vendor/twbs/bootstrap/ - excluded_patterns: - - "*/assets/styles/_*.scss" - - "*/assets/styles/**/_*.scss" -sensiolabs_typescript: - source_dir: - - "%kernel.project_dir%/assets/app" - - "%kernel.project_dir%/assets/controllers" diff --git a/config/packages/cache.php b/config/packages/cache.php new file mode 100644 index 0000000..e2a2091 --- /dev/null +++ b/config/packages/cache.php @@ -0,0 +1,14 @@ +cache() + ->prefixSeed('database_playground/app') + ->app('cache.adapter.redis_tag_aware') + ->defaultRedisProvider(param('app.redis_uri')); +}; diff --git a/config/packages/cache.yaml b/config/packages/cache.yaml deleted file mode 100644 index 5236446..0000000 --- a/config/packages/cache.yaml +++ /dev/null @@ -1,23 +0,0 @@ -framework: - cache: - prefix_seed: "database_playground/app" - - app: cache.adapter.redis_tag_aware - default_redis_provider: "%app.redis_uri%" - # Unique name of your app: used to compute stable namespaces for cache keys. - #prefix_seed: your_vendor_name/app_name - - # The "app" cache stores to the filesystem by default. - # The data in this cache should persist between deploys. - # Other options include: - - # Redis - #app: cache.adapter.redis - #default_redis_provider: redis://localhost - - # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) - #app: cache.adapter.apcu - - # Namespaced pools use the above "app" backend by default - #pools: -#my.dedicated.cache: null diff --git a/config/packages/csrf.php b/config/packages/csrf.php new file mode 100644 index 0000000..352483f --- /dev/null +++ b/config/packages/csrf.php @@ -0,0 +1,16 @@ +form() + ->csrfProtection() + ->tokenId('submit'); + + $frameworkConfig + ->csrfProtection() + ->statelessTokenIds(['submit', 'authenticate', 'logout']); +}; diff --git a/config/packages/csrf.yaml b/config/packages/csrf.yaml deleted file mode 100644 index dd07de8..0000000 --- a/config/packages/csrf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Enable stateless CSRF protection for forms and logins/logouts -framework: - form: - csrf_protection: - token_id: submit - - csrf_protection: - stateless_token_ids: - - submit - - authenticate - - logout diff --git a/config/packages/debug.yaml b/config/packages/debug.yaml deleted file mode 100644 index ce519f3..0000000 --- a/config/packages/debug.yaml +++ /dev/null @@ -1,5 +0,0 @@ -when@dev: - debug: - # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. - # See the "server:dump" command to start a new server. - dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" diff --git a/config/packages/dev/debug.php b/config/packages/dev/debug.php new file mode 100644 index 0000000..457cce8 --- /dev/null +++ b/config/packages/dev/debug.php @@ -0,0 +1,16 @@ +env()) { + // Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. + // See the "server:dump" command to start a new server. + $debugConfig->dumpDestination('tcp://'.env('VAR_DUMPER_SERVER')); + } +}; diff --git a/config/packages/dev/web_profiler.php b/config/packages/dev/web_profiler.php new file mode 100644 index 0000000..d46eba3 --- /dev/null +++ b/config/packages/dev/web_profiler.php @@ -0,0 +1,32 @@ +profiler(); + assert($frameworkProfilerConfig instanceof ProfilerConfig); + + switch ($containerConfigurator->env()) { + case 'dev': + $webProfilerConfig->toolbar(true); + $webProfilerConfig->interceptRedirects(false); + $frameworkProfilerConfig + ->onlyExceptions(false) + ->collectSerializerData(true); + break; + case 'test': + $webProfilerConfig->toolbar(false); + $webProfilerConfig->interceptRedirects(false); + $frameworkProfilerConfig->collect(false); + break; + } +}; diff --git a/config/packages/doctrine.php b/config/packages/doctrine.php new file mode 100644 index 0000000..df3e512 --- /dev/null +++ b/config/packages/doctrine.php @@ -0,0 +1,113 @@ +dbal(); + $dbalConfig + ->connection('default') + ->url(env('DATABASE_URL')->resolve()) + ->profilingCollectBacktrace(param('kernel.debug')) + ->useSavepoints(true); + + $ormConfig = $doctrineConfig->orm(); + $ormConfig + ->autoGenerateProxyClasses(true) + ->enableLazyGhostObjects(true); + + $entityManager = $ormConfig->entityManager('default'); + $entityManager + ->reportFieldsWhereDeclared(true) + ->validateXmlMapping(true) + ->namingStrategy('doctrine.orm.naming_strategy.underscore_number_aware') + ->autoMapping(true) + ->mapping('App', [ + 'type' => 'attribute', + 'is_bundle' => false, + 'dir' => '%kernel.project_dir%/src/Entity', + 'prefix' => 'App\Entity', + 'alias' => 'App', + ]); + + $ormConfig + ->controllerResolver() + ->autoMapping(false); + + $entityManager->dql() + ->datetimeFunction('date', SimpleFunction::class) + ->datetimeFunction('time', SimpleFunction::class) + ->datetimeFunction('timestamp', SimpleFunction::class) + ->datetimeFunction('convert_tz', ConvertTz::class) + ->numericFunction('timestampdiff', TimestampDiff::class) + ->numericFunction('dayofyear', SimpleFunction::class) + ->numericFunction('dayofmonth', SimpleFunction::class) + ->numericFunction('dayofweek', SimpleFunction::class) + ->numericFunction('week', SimpleFunction::class) + ->numericFunction('day', SimpleFunction::class) + ->numericFunction('hour', SimpleFunction::class) + ->numericFunction('minute', SimpleFunction::class) + ->numericFunction('month', SimpleFunction::class) + ->numericFunction('quarter', SimpleFunction::class) + ->numericFunction('second', SimpleFunction::class) + ->numericFunction('year', SimpleFunction::class) + ->numericFunction('sign', Sign::class) + ->numericFunction('pow', Pow::class) + ->numericFunction('round', Round::class) + ->numericFunction('ceil', SimpleFunction::class) + ->stringFunction('md5', SimpleFunction::class) + ->stringFunction('group_concat', GroupConcat::class) + ->stringFunction('concat_ws', ConcatWs::class) + ->stringFunction('cast', Cast::class) + ->stringFunction('replace', Replace::class) + ->stringFunction('date_format', DateFormat::class); + + if ('test' === $containerConfigurator->env()) { + $dbalConfig + ->connection('default') + // "TEST_TOKEN" is typically set by ParaTest + ->dbnameSuffix('_test.%env(default::TEST_TOKEN)%'); + } + + if ('prod' === $containerConfigurator->env()) { + $systemCachePool = 'doctrine.system_cache_pool'; + $resultCachePool = 'doctrine.result_cache_pool'; + + $ormConfig + ->autoGenerateProxyClasses(false) + ->proxyDir('%kernel.build_dir%/doctrine/orm/Proxies'); + + $entityManager + ->queryCacheDriver([ + 'type' => 'pool', + 'pool' => $systemCachePool, + ]); + + $entityManager + ->resultCacheDriver([ + 'type' => 'pool', + 'pool' => $resultCachePool, + ]); + + $cache = $frameworkConfig->cache(); + $cache->pool($systemCachePool)->adapters(['cache.system']); + $cache->pool($resultCachePool)->adapters(['cache.app']); + } +}; diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml deleted file mode 100644 index 4c49cfc..0000000 --- a/config/packages/doctrine.yaml +++ /dev/null @@ -1,82 +0,0 @@ -doctrine: - dbal: - url: "%env(resolve:DATABASE_URL)%" - - # IMPORTANT: You MUST configure your server version, - # either here or in the DATABASE_URL env var (see .env file) - #server_version: '16' - - profiling_collect_backtrace: "%kernel.debug%" - use_savepoints: true - orm: - auto_generate_proxy_classes: true - enable_lazy_ghost_objects: true - report_fields_where_declared: true - validate_xml_mapping: true - naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware - auto_mapping: true - mappings: - App: - type: attribute - is_bundle: false - dir: "%kernel.project_dir%/src/Entity" - prefix: 'App\Entity' - alias: App - controller_resolver: - auto_mapping: false - dql: - datetime_functions: - date: Oro\ORM\Query\AST\Functions\SimpleFunction - time: Oro\ORM\Query\AST\Functions\SimpleFunction - timestamp: Oro\ORM\Query\AST\Functions\SimpleFunction - convert_tz: Oro\ORM\Query\AST\Functions\DateTime\ConvertTz - numeric_functions: - timestampdiff: Oro\ORM\Query\AST\Functions\Numeric\TimestampDiff - dayofyear: Oro\ORM\Query\AST\Functions\SimpleFunction - dayofmonth: Oro\ORM\Query\AST\Functions\SimpleFunction - dayofweek: Oro\ORM\Query\AST\Functions\SimpleFunction - week: Oro\ORM\Query\AST\Functions\SimpleFunction - day: Oro\ORM\Query\AST\Functions\SimpleFunction - hour: Oro\ORM\Query\AST\Functions\SimpleFunction - minute: Oro\ORM\Query\AST\Functions\SimpleFunction - month: Oro\ORM\Query\AST\Functions\SimpleFunction - quarter: Oro\ORM\Query\AST\Functions\SimpleFunction - second: Oro\ORM\Query\AST\Functions\SimpleFunction - year: Oro\ORM\Query\AST\Functions\SimpleFunction - sign: Oro\ORM\Query\AST\Functions\Numeric\Sign - pow: Oro\ORM\Query\AST\Functions\Numeric\Pow - round: Oro\ORM\Query\AST\Functions\Numeric\Round - ceil: Oro\ORM\Query\AST\Functions\SimpleFunction - string_functions: - md5: Oro\ORM\Query\AST\Functions\SimpleFunction - group_concat: Oro\ORM\Query\AST\Functions\String\GroupConcat - concat_ws: Oro\ORM\Query\AST\Functions\String\ConcatWs - cast: Oro\ORM\Query\AST\Functions\Cast - replace: Oro\ORM\Query\AST\Functions\String\Replace - date_format: Oro\ORM\Query\AST\Functions\String\DateFormat - -when@test: - doctrine: - dbal: - # "TEST_TOKEN" is typically set by ParaTest - dbname_suffix: "_test%env(default::TEST_TOKEN)%" - -when@prod: - doctrine: - orm: - auto_generate_proxy_classes: false - proxy_dir: "%kernel.build_dir%/doctrine/orm/Proxies" - query_cache_driver: - type: pool - pool: doctrine.system_cache_pool - result_cache_driver: - type: pool - pool: doctrine.result_cache_pool - - framework: - cache: - pools: - doctrine.result_cache_pool: - adapter: cache.app - doctrine.system_cache_pool: - adapter: cache.system diff --git a/config/packages/doctrine_migrations.php b/config/packages/doctrine_migrations.php new file mode 100644 index 0000000..1927c51 --- /dev/null +++ b/config/packages/doctrine_migrations.php @@ -0,0 +1,15 @@ +enableProfiler(false); + + $doctrineMigrationsConfig + // namespace is arbitrary but should be different from App\Migrations + // as migrations classes should NOT be autoloaded + ->migrationsPath('DoctrineMigrations', '%kernel.project_dir%/migrations'); +}; diff --git a/config/packages/doctrine_migrations.yaml b/config/packages/doctrine_migrations.yaml deleted file mode 100644 index 9300c9b..0000000 --- a/config/packages/doctrine_migrations.yaml +++ /dev/null @@ -1,6 +0,0 @@ -doctrine_migrations: - migrations_paths: - # namespace is arbitrary but should be different from App\Migrations - # as migrations classes should NOT be autoloaded - "DoctrineMigrations": "%kernel.project_dir%/migrations" - enable_profiler: false diff --git a/config/packages/framework.php b/config/packages/framework.php new file mode 100644 index 0000000..0f2b85e --- /dev/null +++ b/config/packages/framework.php @@ -0,0 +1,34 @@ +session(); + assert($sessionConfig instanceof SessionConfig); + + $frameworkConfig->secret(env('APP_SECRET')); + + // Note that the session will be started ONLY if you read or write from it. + $sessionConfig->handlerId(env('REDIS_URI')); + + // proxy configuration for Zeabur + $frameworkConfig->trustedProxies('private_ranges'); + $frameworkConfig->trustedHeaders([ + 'x-forwarded-for', + 'x-forwarded-host', + 'x-forwarded-proto', + 'x-forwarded-port', + 'x-forwarded-prefix', + ]); + + if ('test' === $containerConfigurator->env()) { + $frameworkConfig->test(true); + $sessionConfig->storageFactoryId('session.storage.factory.mock_file'); + } +}; diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml deleted file mode 100644 index 2f9aceb..0000000 --- a/config/packages/framework.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# see https://symfony.com/doc/current/reference/configuration/framework.html -framework: - secret: "%env(APP_SECRET)%" - #csrf_protection: true - - # Note that the session will be started ONLY if you read or write from it. - session: - handler_id: "%env(REDIS_URI)%" - - #esi: true - #fragments: true - - # proxy configuration for Zeabur - trusted_proxies: "private_ranges" - trusted_headers: [ - "x-forwarded-for", - "x-forwarded-host", - "x-forwarded-proto", - "x-forwarded-port", - "x-forwarded-prefix", - ] - -when@test: - framework: - test: true - session: - storage_factory_id: session.storage.factory.mock_file diff --git a/config/packages/http_discovery.php b/config/packages/http_discovery.php new file mode 100644 index 0000000..303701a --- /dev/null +++ b/config/packages/http_discovery.php @@ -0,0 +1,22 @@ +services() + ->alias(RequestFactoryInterface::class, Psr17Factory::class) + ->alias(ResponseFactoryInterface::class, Psr17Factory::class) + ->alias(ServerRequestFactoryInterface::class, Psr17Factory::class) + ->alias(StreamFactoryInterface::class, Psr17Factory::class) + ->alias(UploadedFileFactoryInterface::class, Psr17Factory::class) + ->alias(UriFactoryInterface::class, Psr17Factory::class); +}; diff --git a/config/packages/http_discovery.yaml b/config/packages/http_discovery.yaml deleted file mode 100644 index 3e7ef4b..0000000 --- a/config/packages/http_discovery.yaml +++ /dev/null @@ -1,10 +0,0 @@ -services: - Psr\Http\Message\RequestFactoryInterface: "@http_discovery.psr17_factory" - Psr\Http\Message\ResponseFactoryInterface: "@http_discovery.psr17_factory" - Psr\Http\Message\ServerRequestFactoryInterface: "@http_discovery.psr17_factory" - Psr\Http\Message\StreamFactoryInterface: "@http_discovery.psr17_factory" - Psr\Http\Message\UploadedFileFactoryInterface: "@http_discovery.psr17_factory" - Psr\Http\Message\UriFactoryInterface: "@http_discovery.psr17_factory" - - http_discovery.psr17_factory: - class: Http\Discovery\Psr17Factory diff --git a/config/packages/lock.yaml b/config/packages/lock.yaml deleted file mode 100644 index 6c7aa58..0000000 --- a/config/packages/lock.yaml +++ /dev/null @@ -1,2 +0,0 @@ -framework: - lock: "%env(LOCK_DSN)%" diff --git a/config/packages/mailer.php b/config/packages/mailer.php new file mode 100644 index 0000000..e9df49e --- /dev/null +++ b/config/packages/mailer.php @@ -0,0 +1,12 @@ +mailer() + ->dsn(env('MAILER_DSN')); +}; diff --git a/config/packages/mailer.yaml b/config/packages/mailer.yaml deleted file mode 100644 index 813f6e1..0000000 --- a/config/packages/mailer.yaml +++ /dev/null @@ -1,3 +0,0 @@ -framework: - mailer: - dsn: "%env(MAILER_DSN)%" diff --git a/config/packages/meilisearch.php b/config/packages/meilisearch.php new file mode 100644 index 0000000..df18cf2 --- /dev/null +++ b/config/packages/meilisearch.php @@ -0,0 +1,27 @@ +url(env('MEILISEARCH_URL')) + ->apiKey(env('MEILISEARCH_API_KEY')) + ->indices() + ->name('questions') + ->class(Question::class) + ->enableSerializerGroups(true) + ->settings([ + 'filterableAttributes' => [ + 'type', + 'difficulty', + ], + 'sortableAttributes' => [ + 'id', + ], + ]); +}; diff --git a/config/packages/meilisearch.yaml b/config/packages/meilisearch.yaml deleted file mode 100644 index 1b9d805..0000000 --- a/config/packages/meilisearch.yaml +++ /dev/null @@ -1,17 +0,0 @@ -meilisearch: - url: "%env(MEILISEARCH_URL)%" # URL of the Meilisearch server (mandatory) - api_key: "%env(MEILISEARCH_API_KEY)%" # API key to access the Meilisearch server (mandatory) - indices: - - name: questions - class: App\Entity\Question - enable_serializer_groups: true - settings: - filterableAttributes: - - type - - difficulty - sortableAttributes: - - id - -when@preprod: - meilisearch: - prefix: prod_ diff --git a/config/packages/messenger.php b/config/packages/messenger.php new file mode 100644 index 0000000..e501458 --- /dev/null +++ b/config/packages/messenger.php @@ -0,0 +1,40 @@ +messenger(); + + $messenger->failureTransport('failed'); + + $asyncTransport = $messenger->transport('async'); + assert($asyncTransport instanceof TransportConfig); + $asyncTransport->dsn(env('MESSENGER_TRANSPORT_DSN')); + $asyncTransport->retryStrategy() + ->maxRetries(3) + ->multiplier(2); + + $failedTransport = $messenger->transport('failed'); + assert($failedTransport instanceof TransportConfig); + $failedTransport->dsn('doctrine://default?queue_name=failed'); + + $messenger->bus('messenger.bus.default'); + $messenger->defaultBus('messenger.bus.default'); + + $sendEmailMessageRoutingConfig = $messenger->routing(SendEmailMessage::class); + assert($sendEmailMessageRoutingConfig instanceof RoutingConfig); + $sendEmailMessageRoutingConfig->senders(['async']); + + $notifierRoutingConfig = $messenger->routing(ChatMessage::class); + assert($notifierRoutingConfig instanceof RoutingConfig); + $notifierRoutingConfig->senders(['async']); +}; diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml deleted file mode 100644 index fd7f8cd..0000000 --- a/config/packages/messenger.yaml +++ /dev/null @@ -1,27 +0,0 @@ -framework: - messenger: - failure_transport: failed - - transports: - # https://symfony.com/doc/current/messenger.html#transport-configuration - async: - dsn: "%env(MESSENGER_TRANSPORT_DSN)%" - options: - use_notify: true - check_delayed_interval: 60000 - retry_strategy: - max_retries: 3 - multiplier: 2 - failed: "doctrine://default?queue_name=failed" - # sync: 'sync://' - - default_bus: messenger.bus.default - - buses: - messenger.bus.default: [] - - routing: - Symfony\Component\Mailer\Messenger\SendEmailMessage: async - - # Route your messages to the transports - # 'App\Message\YourMessage': async diff --git a/config/packages/mjml.php b/config/packages/mjml.php new file mode 100644 index 0000000..14e637e --- /dev/null +++ b/config/packages/mjml.php @@ -0,0 +1,10 @@ +options([ + 'binary' => '%kernel.project_dir%/node_modules/.bin/mjml', + 'minify' => false, + ]); +}; diff --git a/config/packages/monolog.php b/config/packages/monolog.php new file mode 100644 index 0000000..51dfa3d --- /dev/null +++ b/config/packages/monolog.php @@ -0,0 +1,67 @@ +channels(['deprecation']); + + if ('dev' === $containerConfigurator->env()) { + $monologConfig->handler('main') + ->type('stream') + ->path('%kernel.logs_dir%/%kernel.environment%.log') + ->level('debug') + ->channels() + ->elements(['!event']); + $monologConfig->handler('console') + ->type('console') + ->processPsr3Messages(false) + ->channels() + ->elements(['!event', '!doctrine', '!console']); + } + + if ('test' === $containerConfigurator->env()) { + $monologConfig->handler('main') + ->type('fingers_crossed') + ->actionLevel('error') + ->handler('nested') + ->excludedHttpCode(404) + ->excludedHttpCode(405) + ->channels() + ->elements(['!event']); + $monologConfig->handler('nested') + ->type('stream') + ->path('%kernel.logs_dir%/%kernel.environment%.log') + ->level('debug'); + } + + if ('prod' === $containerConfigurator->env()) { + $monologConfig->handler('main') + ->type('fingers_crossed') + ->actionLevel('error') + ->handler('nested') + ->excludedHttpCode(404) + ->excludedHttpCode(405) + // How many messages should be saved? Prevent memory leaks + ->bufferSize(50); + $monologConfig->handler('nested') + ->type('stream') + ->path('php://stderr') + ->level('debug') + ->formatter('monolog.formatter.json'); + $monologConfig->handler('console') + ->type('console') + ->processPsr3Messages(false) + ->channels() + ->elements(['!event', '!doctrine']); + $monologConfig->handler('deprecation') + ->type('stream') + ->path('php://stderr') + ->formatter('monolog.formatter.json') + ->channels() + ->elements(['deprecation']); + } +}; diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml deleted file mode 100644 index c2da281..0000000 --- a/config/packages/monolog.yaml +++ /dev/null @@ -1,62 +0,0 @@ -monolog: - channels: - - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists - -when@dev: - monolog: - handlers: - main: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: debug - channels: ["!event"] - # uncomment to get logging in your browser - # you may have to allow bigger header sizes in your Web server configuration - #firephp: - # type: firephp - # level: info - #chromephp: - # type: chromephp - # level: info - console: - type: console - process_psr_3_messages: false - channels: ["!event", "!doctrine", "!console"] - -when@test: - monolog: - handlers: - main: - type: fingers_crossed - action_level: error - handler: nested - excluded_http_codes: [404, 405] - channels: ["!event"] - nested: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: debug - -when@prod: - monolog: - handlers: - main: - type: fingers_crossed - action_level: error - handler: nested - excluded_http_codes: [404, 405] - buffer_size: 50 # How many messages should be saved? Prevent memory leaks - nested: - type: stream - path: php://stderr - level: debug - formatter: monolog.formatter.json - console: - type: console - process_psr_3_messages: false - channels: ["!event", "!doctrine"] - deprecation: - type: stream - channels: [deprecation] - path: php://stderr - formatter: monolog.formatter.json diff --git a/config/packages/notifier.php b/config/packages/notifier.php new file mode 100644 index 0000000..e321b1f --- /dev/null +++ b/config/packages/notifier.php @@ -0,0 +1,15 @@ +notifier(); + + $notifierConfig->chatterTransport('linenotify', env('LINE_NOTIFY_DSN')); + $notifierConfig->adminRecipient()->email('dbplay@pan93.com'); +}; diff --git a/config/packages/notifier.yaml b/config/packages/notifier.yaml deleted file mode 100644 index 8d4eff7..0000000 --- a/config/packages/notifier.yaml +++ /dev/null @@ -1,12 +0,0 @@ -framework: - notifier: - chatter_transports: - linenotify: "%env(LINE_NOTIFY_DSN)%" - texter_transports: - channel_policy: - urgent: ["chat/linenotify"] - high: ["chat/linenotify"] - medium: ["chat/linenotify"] - low: ["chat/linenotify"] - admin_recipients: - - { email: dbplay@pan93.com } diff --git a/config/packages/routing.php b/config/packages/routing.php new file mode 100644 index 0000000..71c1387 --- /dev/null +++ b/config/packages/routing.php @@ -0,0 +1,18 @@ +router(); + assert($routerConfig instanceof RouterConfig); // workaround for PHPStan support + + $routerConfig->strictRequirements(true); + + if ('prod' === $containerConfigurator->env()) { + $routerConfig->strictRequirements(null); + } +}; diff --git a/config/packages/routing.yaml b/config/packages/routing.yaml deleted file mode 100644 index 5969ced..0000000 --- a/config/packages/routing.yaml +++ /dev/null @@ -1,10 +0,0 @@ -framework: - router: -# Configure how to generate URLs in non-HTTP contexts, such as CLI commands. -# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands -#default_uri: http://localhost - -when@prod: - framework: - router: - strict_requirements: null diff --git a/config/packages/security.php b/config/packages/security.php new file mode 100644 index 0000000..566c37b --- /dev/null +++ b/config/packages/security.php @@ -0,0 +1,87 @@ +passwordHasher(PasswordAuthenticatedUserInterface::class, 'auto'); + + // used to reload user from session & other features (e.g. switch_user) + $securityConfig + ->provider('app_user_provider') + ->entity() + ->class(User::class) + ->property('email'); + + $securityConfig + ->firewall('dev') + ->pattern('^/(_(profiler|wdt)|css|images|js)/') + ->security(false); + + $mainFirewall = $securityConfig->firewall('main'); + + $mainFirewall + ->lazy(true) + ->provider('app_user_provider'); + + $mainFirewall + ->formLogin() + ->loginPath('app_login') + ->checkPath('app_login') + ->enableCsrf(true); + + $mainFirewall + ->logout() + ->path('app_logout') + ->target('app_home'); + + $mainFirewall + ->rememberMe() + ->secret(param('kernel.secret')) + ->lifetime(604800 /* 1 week in seconds */); + + // https://symfony.com/doc/current/security/impersonating_user.html + $mainFirewall->switchUser(); + + // Allow anonymous access to the login form. + $securityConfig + ->accessControl() + ->route('app_login') + ->roles('PUBLIC_ACCESS'); + + // Allow anonymous access to the feedback form. + $securityConfig + ->accessControl() + ->route('app_feedback') + ->roles('PUBLIC_ACCESS'); + + // Admin + $securityConfig + ->accessControl() + ->path('^/admin') + ->roles('ROLE_ADMIN'); + + // Others (for example, apps) + $securityConfig + ->accessControl() + ->path('^/') + ->roles('ROLE_USER'); + + if ('test' === $containerConfigurator->env()) { + $passwordHasher = $securityConfig->passwordHasher(PasswordAuthenticatedUserInterface::class); + assert($passwordHasher instanceof PasswordHasherConfig); + + $passwordHasher + ->algorithm('auto') + ->cost(4) + ->timeCost(3) + ->memoryCost(10); + } +}; diff --git a/config/packages/security.yaml b/config/packages/security.yaml deleted file mode 100644 index 6a6fdee..0000000 --- a/config/packages/security.yaml +++ /dev/null @@ -1,64 +0,0 @@ -security: - # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords - password_hashers: - Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: "auto" - # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider - providers: - # used to reload user from session & other features (e.g. switch_user) - app_user_provider: - entity: - class: App\Entity\User - property: email - firewalls: - dev: - pattern: ^/(_(profiler|wdt)|css|images|js)/ - security: false - main: - lazy: true - provider: app_user_provider - form_login: - login_path: app_login - check_path: app_login - enable_csrf: true - logout: - path: app_logout - target: app_home - remember_me: - secret: "%kernel.secret%" # required - lifetime: 604800 # 1 week in seconds - - # https://symfony.com/doc/current/security/impersonating_user.html - switch_user: true - - # activate different ways to authenticate - # https://symfony.com/doc/current/security.html#the-firewall - - # Easy way to control access for large sections of your site - # Note: Only the *first* access control that matches will be used - access_control: - # login page - - { route: app_login, roles: PUBLIC_ACCESS } - - # feedback page - # Note that we provide the feedback form in login page, - # so we need to allow public access to this page. - - { route: app_feedback, roles: PUBLIC_ACCESS } - - # admin - - { path: ^/admin, roles: ROLE_ADMIN } - - # others (for example, apps) - - { path: ^/, roles: ROLE_USER } - -when@test: - security: - password_hashers: - # By default, password hashers are resource intensive and take time. This is - # important to generate secure password hashes. In tests however, secure hashes - # are not important, waste resources and increase test times. The following - # reduces the work factor to the lowest possible values. - Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: - algorithm: auto - cost: 4 # Lowest possible value for bcrypt - time_cost: 3 # Lowest possible value for argon - memory_cost: 10 # Lowest possible value for argon diff --git a/config/packages/sensiolabs_typescript.php b/config/packages/sensiolabs_typescript.php new file mode 100644 index 0000000..20830b9 --- /dev/null +++ b/config/packages/sensiolabs_typescript.php @@ -0,0 +1,13 @@ +swcBinary('node_modules/.bin/swc'); + $config->sourceDir([ + '%kernel.project_dir%/assets/app', + '%kernel.project_dir%/assets/controllers', + ]); +}; diff --git a/config/packages/sensiolabs_typescript.yaml b/config/packages/sensiolabs_typescript.yaml deleted file mode 100644 index 604fcbc..0000000 --- a/config/packages/sensiolabs_typescript.yaml +++ /dev/null @@ -1,2 +0,0 @@ -sensiolabs_typescript: - swc_binary: "node_modules/.bin/swc" diff --git a/config/packages/translation.php b/config/packages/translation.php new file mode 100644 index 0000000..d9ec9b0 --- /dev/null +++ b/config/packages/translation.php @@ -0,0 +1,16 @@ +defaultLocale('zh_TW') + ->enabledLocales(['zh_TW']); + + $config + ->translator() + ->defaultPath('%kernel.project_dir%/translations') + ->fallbacks(['zh_TW']); +}; diff --git a/config/packages/translation.yaml b/config/packages/translation.yaml deleted file mode 100644 index 6fdb6e9..0000000 --- a/config/packages/translation.yaml +++ /dev/null @@ -1,8 +0,0 @@ -framework: - default_locale: zh_TW - enabled_locales: ["zh_TW"] - translator: - default_path: "%kernel.project_dir%/translations" - fallbacks: - - zh_TW - providers: diff --git a/config/packages/twig.php b/config/packages/twig.php new file mode 100644 index 0000000..15f19b3 --- /dev/null +++ b/config/packages/twig.php @@ -0,0 +1,19 @@ +fileNamePattern('*.twig') + ->formThemes(['bootstrap_5_layout.html.twig']) + ->global('umami_domain', env('UMAMI_DOMAIN')) + ->global('umami_website_id', env('UMAMI_WEBSITE_ID')) + ->strictVariables(true) + ->global('app_features_hint', param('app.features.hint')) + ->global('app_features_comment', param('app.features.comment')); +}; diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml deleted file mode 100644 index 4af147b..0000000 --- a/config/packages/twig.yaml +++ /dev/null @@ -1,13 +0,0 @@ -twig: - file_name_pattern: "*.twig" - form_themes: ["bootstrap_5_layout.html.twig"] - globals: - umami_domain: "%env(UMAMI_DOMAIN)%" - umami_website_id: "%env(UMAMI_WEBSITE_ID)%" - appfeatures: - hint: "%app.features.hint%" - comment: "%app.features.comment%" - -when@test: - twig: - strict_variables: true diff --git a/config/packages/twig_component.php b/config/packages/twig_component.php new file mode 100644 index 0000000..42605b9 --- /dev/null +++ b/config/packages/twig_component.php @@ -0,0 +1,11 @@ +anonymousTemplateDirectory('components/') + ->defaults('App\Twig\Components\\', 'components/'); +}; diff --git a/config/packages/twig_component.yaml b/config/packages/twig_component.yaml deleted file mode 100644 index 7b4b5eb..0000000 --- a/config/packages/twig_component.yaml +++ /dev/null @@ -1,5 +0,0 @@ -twig_component: - anonymous_template_directory: "components/" - defaults: - # Namespace & directory for components - App\Twig\Components\: "components/" diff --git a/config/packages/validator.php b/config/packages/validator.php new file mode 100644 index 0000000..c4f1adc --- /dev/null +++ b/config/packages/validator.php @@ -0,0 +1,11 @@ +validation()->notCompromisedPassword(); + $notCompromisedPassword->enabled('test' !== $containerConfigurator->env()); +}; diff --git a/config/packages/validator.yaml b/config/packages/validator.yaml deleted file mode 100644 index 7b5d7d5..0000000 --- a/config/packages/validator.yaml +++ /dev/null @@ -1,11 +0,0 @@ -framework: - validation: -# Enables validator auto-mapping support. -# For instance, basic validation constraints will be inferred from Doctrine's metadata. -#auto_mapping: -# App\Entity\: [] - -when@test: - framework: - validation: - not_compromised_password: false diff --git a/config/packages/web_profiler.yaml b/config/packages/web_profiler.yaml deleted file mode 100644 index a529921..0000000 --- a/config/packages/web_profiler.yaml +++ /dev/null @@ -1,17 +0,0 @@ -when@dev: - web_profiler: - toolbar: true - intercept_redirects: false - - framework: - profiler: - only_exceptions: false - collect_serializer_data: true - -when@test: - web_profiler: - toolbar: false - intercept_redirects: false - - framework: - profiler: { collect: false } diff --git a/config/routes.php b/config/routes.php new file mode 100644 index 0000000..d3a5ab6 --- /dev/null +++ b/config/routes.php @@ -0,0 +1,12 @@ +import([ + 'path' => '../src/Controller/', + 'namespace' => 'App\Controller', + ], 'attribute'); +}; diff --git a/config/routes.yaml b/config/routes.yaml deleted file mode 100644 index 2d0ef99..0000000 --- a/config/routes.yaml +++ /dev/null @@ -1,5 +0,0 @@ -controllers: - resource: - path: ../src/Controller/ - namespace: App\Controller - type: attribute diff --git a/config/routes/easyadmin.php b/config/routes/easyadmin.php new file mode 100644 index 0000000..ddb8599 --- /dev/null +++ b/config/routes/easyadmin.php @@ -0,0 +1,9 @@ +import('.', 'easyadmin.routes'); +}; diff --git a/config/routes/easyadmin.yaml b/config/routes/easyadmin.yaml deleted file mode 100644 index f409de2..0000000 --- a/config/routes/easyadmin.yaml +++ /dev/null @@ -1,3 +0,0 @@ -easyadmin: - resource: . - type: easyadmin.routes diff --git a/config/routes/framework.php b/config/routes/framework.php new file mode 100644 index 0000000..455101b --- /dev/null +++ b/config/routes/framework.php @@ -0,0 +1,12 @@ +env()) { + $routingConfigurator->import('@FrameworkBundle/Resources/config/routing/errors.xml') + ->prefix('/_error'); + } +}; diff --git a/config/routes/framework.yaml b/config/routes/framework.yaml deleted file mode 100644 index cce01c1..0000000 --- a/config/routes/framework.yaml +++ /dev/null @@ -1,4 +0,0 @@ -when@dev: - _errors: - resource: "@FrameworkBundle/Resources/config/routing/errors.xml" - prefix: /_error diff --git a/config/routes/security.php b/config/routes/security.php new file mode 100644 index 0000000..cd2c44f --- /dev/null +++ b/config/routes/security.php @@ -0,0 +1,9 @@ +import('security.route_loader.logout', 'service'); +}; diff --git a/config/routes/security.yaml b/config/routes/security.yaml deleted file mode 100644 index 3cb5ef0..0000000 --- a/config/routes/security.yaml +++ /dev/null @@ -1,3 +0,0 @@ -_security_logout: - resource: security.route_loader.logout - type: service diff --git a/config/routes/ux_live_component.php b/config/routes/ux_live_component.php new file mode 100644 index 0000000..e7cb167 --- /dev/null +++ b/config/routes/ux_live_component.php @@ -0,0 +1,10 @@ +import('@LiveComponentBundle/config/routes.php') + ->prefix('/_components'); +}; diff --git a/config/routes/ux_live_component.yaml b/config/routes/ux_live_component.yaml deleted file mode 100644 index b062f61..0000000 --- a/config/routes/ux_live_component.yaml +++ /dev/null @@ -1,5 +0,0 @@ -live_component: - resource: "@LiveComponentBundle/config/routes.php" - prefix: "/_components" - # adjust prefix to add localization to your components - #prefix: '/{_locale}/_components' diff --git a/config/routes/web_profiler.php b/config/routes/web_profiler.php new file mode 100644 index 0000000..63bbf5f --- /dev/null +++ b/config/routes/web_profiler.php @@ -0,0 +1,14 @@ +env()) { + $routingConfigurator->import('@WebProfilerBundle/Resources/config/routing/wdt.xml') + ->prefix('/_wdt'); + $routingConfigurator->import('@WebProfilerBundle/Resources/config/routing/profiler.xml') + ->prefix('/_profiler'); + } +}; diff --git a/config/routes/web_profiler.yaml b/config/routes/web_profiler.yaml deleted file mode 100644 index e004603..0000000 --- a/config/routes/web_profiler.yaml +++ /dev/null @@ -1,8 +0,0 @@ -when@dev: - web_profiler_wdt: - resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml" - prefix: /_wdt - - web_profiler_profiler: - resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml" - prefix: /_profiler diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..ff7f650 --- /dev/null +++ b/config/services.php @@ -0,0 +1,51 @@ +parameters() + ->set('app.sqlrunner_url', env('SQLRUNNER_URL')) + ->set('app.redis_uri', env('REDIS_URI')) + ->set('app.openai_api_key', env('OPENAI_API_KEY')) + ->set('app.server-mail', env('SERVER_EMAIL')) + ->set('app.features.hint', true) + ->set('app.features.editable-profile', true) + ->set('app.features.comment', true); + + $services = $containerConfigurator->services(); + + $services->defaults() + ->autowire() + ->autoconfigure(); + + $services->load('App\\', __DIR__.'/../src/') + ->exclude([ + __DIR__.'/../src/DependencyInjection/', + __DIR__.'/../src/Entity/', + __DIR__.'/../src/Kernel.php', + __DIR__.'/../src/Service/Processes/', + __DIR__.'/../src/Service/Types/', + __DIR__.'/../src/Twig/Components/Challenge/EventConstant.php', + ]); + + $services->set(PromptService::class) + ->arg('$apiKey', param('app.openai_api_key')); + + $services->set(SqlRunnerService::class) + ->arg('$baseUrl', param('app.sqlrunner_url')); + + $services->set(EmailService::class) + ->arg('$serverMail', param('app.server-mail')); + + $services->set(EmailTemplateController::class) + ->arg('$projectDir', param('kernel.project_dir')); +}; diff --git a/config/services.yaml b/config/services.yaml deleted file mode 100644 index 0641d8b..0000000 --- a/config/services.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# This file is the entry point to configure your own services. -# Files in the packages/ subdirectory configure your dependencies. - -# Put parameters here that don't need to change on each machine where the app is deployed -# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration -parameters: - app.sqlrunner_url: "%env(SQLRUNNER_URL)%" - app.redis_uri: "%env(REDIS_URI)%" - app.openai_api_key: "%env(OPENAI_API_KEY)%" - - app.features.hint: true - app.features.editable-profile: true - app.features.comment: true - -services: - # default configuration for services in *this* file - _defaults: - autowire: true # Automatically injects dependencies in your services. - autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. - - # makes classes in src/ available to be used as services - # this creates a service per class whose id is the fully-qualified class name - App\: - resource: "../src/" - exclude: - - "../src/DependencyInjection/" - - "../src/Entity/" - - "../src/Kernel.php" - - "../src/Service/Processes/" - - "../src/Service/Types/" - - "../src/Twig/Components/Challenge/EventConstant.php" - - # add more service definitions when explicit configuration is needed - # please note that last definitions always *replace* previous ones - App\Service\PromptService: - arguments: - $apiKey: "%app.openai_api_key%" - - App\Service\SqlRunnerService: - arguments: - $baseUrl: "%app.sqlrunner_url%" diff --git a/devenv.lock b/devenv.lock index 6b5f771..8d858b8 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,10 +3,10 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1732896163, + "lastModified": 1733595596, "owner": "cachix", "repo": "devenv", - "rev": "2c928a199d56191d7a53f29ccafa56238c8ce4e5", + "rev": "c1ca69791bfa466e77b3d21e0e2f492810d83250", "type": "github" }, "original": { @@ -19,10 +19,10 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1732722421, + "lastModified": 1733328505, "owner": "edolstra", "repo": "flake-compat", - "rev": "9ed2ac151eada2306ca8c418ebd97807bb08f6ac", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", "type": "github" }, "original": { @@ -53,10 +53,10 @@ }, "nixpkgs": { "locked": { - "lastModified": 1733064805, + "lastModified": 1733376361, "owner": "nixos", "repo": "nixpkgs", - "rev": "31d66ae40417bb13765b0ad75dd200400e98de84", + "rev": "929116e316068c7318c54eb4d827f7d9756d5e9c", "type": "github" }, "original": { @@ -68,10 +68,10 @@ }, "nixpkgs-stable": { "locked": { - "lastModified": 1733016324, + "lastModified": 1733384649, "owner": "NixOS", "repo": "nixpkgs", - "rev": "7e1ca67996afd8233d9033edd26e442836cc2ad6", + "rev": "190c31a89e5eec80dd6604d7f9e5af3802a58a13", "type": "github" }, "original": { @@ -91,10 +91,10 @@ "nixpkgs-stable": "nixpkgs-stable" }, "locked": { - "lastModified": 1732021966, + "lastModified": 1733665616, "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "3308484d1a443fc5bc92012435d79e80458fe43c", + "rev": "d8c02f0ffef0ef39f6063731fc539d8c71eb463a", "type": "github" }, "original": { diff --git a/frankenphp/docker-entrypoint.sh b/frankenphp/docker-entrypoint.sh index 5af3f41..7e2d49a 100644 --- a/frankenphp/docker-entrypoint.sh +++ b/frankenphp/docker-entrypoint.sh @@ -7,39 +7,45 @@ if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then fi if [ -z "$(ls -A 'node_modules/' 2>/dev/null)" ]; then - corepnpm prepare && pnpm install --prod --prefer-frozen-lockfile - fi - - if grep -q ^DATABASE_URL= .env; then - echo "Waiting for database to be ready..." - ATTEMPTS_LEFT_TO_REACH_DATABASE=60 - until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do - if [ $? -eq 255 ]; then - # If the Doctrine command exits with 255, an unrecoverable error occurred - ATTEMPTS_LEFT_TO_REACH_DATABASE=0 - break - fi - sleep 1 - ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1)) - echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left." - done - - if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then - echo "The database is not up or not reachable:" - echo "$DATABASE_ERROR" - exit 1 - else - echo "The database is now ready and reachable" - fi - - if [ "$( find ./migrations -iname '*.php' -print -quit )" ]; then - php bin/console doctrine:migrations:migrate --no-interaction --all-or-nothing - fi + if node -v; then + corepack prepare && pnpm install --prod --prefer-frozen-lockfile + else + echo "Node.js is not installed. Skipping npm packages installation." + fi fi - echo "Updating Meilisearch indexes..." - php bin/console meili:clear || true - php bin/console meili:import --update-settings || true + if [ "${RUN_MIGRATIONS:-true}" = "true" ]; then + if grep -q ^DATABASE_URL= .env; then + echo "Waiting for database to be ready..." + ATTEMPTS_LEFT_TO_REACH_DATABASE=60 + until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do + if [ $? -eq 255 ]; then + # If the Doctrine command exits with 255, an unrecoverable error occurred + ATTEMPTS_LEFT_TO_REACH_DATABASE=0 + break + fi + sleep 1 + ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1)) + echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left." + done + + if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then + echo "The database is not up or not reachable:" + echo "$DATABASE_ERROR" + exit 1 + else + echo "The database is now ready and reachable" + fi + + if [ "$( find ./migrations -iname '*.php' -print -quit )" ]; then + php bin/console doctrine:migrations:migrate --no-interaction --all-or-nothing + fi + fi + + echo "Updating Meilisearch indexes..." + php bin/console meili:clear || true + php bin/console meili:import --update-settings || true + fi setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var diff --git a/importmap.php b/importmap.php index c37c8d7..e648e4c 100644 --- a/importmap.php +++ b/importmap.php @@ -50,10 +50,10 @@ 'version' => '6.8.0', ], '@codemirror/view' => [ - 'version' => '6.35.0', + 'version' => '6.35.3', ], '@codemirror/state' => [ - 'version' => '6.4.1', + 'version' => '6.5.0', ], '@codemirror/language' => [ 'version' => '6.10.6', @@ -91,4 +91,7 @@ '@kurkle/color' => [ 'version' => '0.3.4', ], + '@marijn/find-cluster-break' => [ + 'version' => '1.0.0', + ], ]; diff --git a/migrations/Version20241204160558.php b/migrations/Version20241204160558.php new file mode 100644 index 0000000..b0a009a --- /dev/null +++ b/migrations/Version20241204160558.php @@ -0,0 +1,90 @@ +addSql(<<<'SQL' + CREATE SEQUENCE email_id_seq INCREMENT BY 1 MINVALUE 1 START 1 + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE email ( + id INT NOT NULL, + subject VARCHAR(4096) NOT NULL, + content TEXT NOT NULL, + kind VARCHAR(255) NOT NULL, + PRIMARY KEY(id) + ) + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE email_delivery_event ( + id UUID NOT NULL, + to_user_id INT DEFAULT NULL, + email_id INT NOT NULL, + created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, + to_address VARCHAR(512) NOT NULL, + PRIMARY KEY(id) + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_F35AF0FC29F6EE60 ON email_delivery_event (to_user_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_F35AF0FCA832C1C9 ON email_delivery_event (email_id) + SQL); + $this->addSql(<<<'SQL' + COMMENT ON COLUMN email_delivery_event.id IS '(DC2Type:ulid)' + SQL); + $this->addSql(<<<'SQL' + COMMENT ON COLUMN email_delivery_event.created_at IS '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE + email_delivery_event + ADD + CONSTRAINT FK_F35AF0FC29F6EE60 FOREIGN KEY (to_user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE + email_delivery_event + ADD + CONSTRAINT FK_F35AF0FCA832C1C9 FOREIGN KEY (email_id) REFERENCES email (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + DROP SEQUENCE email_id_seq CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE email_delivery_event DROP CONSTRAINT FK_F35AF0FC29F6EE60 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE email_delivery_event DROP CONSTRAINT FK_F35AF0FCA832C1C9 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE email + SQL); + $this->addSql(<<<'SQL' + DROP TABLE email_delivery_event + SQL); + } +} diff --git a/migrations/Version20241208144321.php b/migrations/Version20241208144321.php new file mode 100644 index 0000000..ac2ce25 --- /dev/null +++ b/migrations/Version20241208144321.php @@ -0,0 +1,41 @@ +addSql(<<<'SQL' + ALTER TABLE email ADD text_content TEXT NOT NULL DEFAULT '' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE email RENAME COLUMN content TO html_content + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE email RENAME COLUMN html_content TO content + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE email DROP text_content + SQL); + } +} diff --git a/migrations/Version20241208190542.php b/migrations/Version20241208190542.php new file mode 100644 index 0000000..eb6bce8 --- /dev/null +++ b/migrations/Version20241208190542.php @@ -0,0 +1,38 @@ +addSql(<<<'SQL' + ALTER TABLE email ALTER text_content DROP DEFAULT + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + CREATE SCHEMA public + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE email ALTER text_content SET DEFAULT '' + SQL); + } +} diff --git a/package.json b/package.json index b76c669..bdb5a4d 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,13 @@ }, "dependencies": { "@swc/cli": "^0.5.2", - "@swc/core": "^1.9.3" + "@swc/core": "^1.10.1", + "mjml": "^4.15.3" }, "packageManager": "pnpm@9.14.4+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab", "devDependencies": { "@codemirror/lang-sql": "^6.8.0", - "@codemirror/state": "^6.4.1", + "@codemirror/state": "^6.5.0", "@eslint/js": "^9.16.0", "@hotwired/stimulus": "^3.2.2", "@symfony/stimulus-bridge": "^3.2.2", diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 2469fa7..98a23ff 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -6,5 +6,12 @@ parameters: - public/ - src/ - tests/ + scanDirectories: + - var/cache/dev/Symfony/Config + scanFiles: + - vendor/symfony/dependency-injection/Loader/Configurator/ContainerConfigurator.php symfony: consoleApplicationLoader: tests/console-application.php + containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml + doctrine: + objectManagerLoader: tests/object-manager.php diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b10269e..05506d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,17 +9,20 @@ importers: dependencies: "@swc/cli": specifier: ^0.5.2 - version: 0.5.2(@swc/core@1.9.3) + version: 0.5.2(@swc/core@1.10.1)(chokidar@3.6.0) "@swc/core": - specifier: ^1.9.3 - version: 1.9.3 + specifier: ^1.10.1 + version: 1.10.1 + mjml: + specifier: ^4.15.3 + version: 4.15.3 devDependencies: "@codemirror/lang-sql": specifier: ^6.8.0 - version: 6.8.0(@codemirror/view@6.35.0) + version: 6.8.0(@codemirror/view@6.35.3) "@codemirror/state": - specifier: ^6.4.1 - version: 6.4.1 + specifier: ^6.5.0 + version: 6.5.0 "@eslint/js": specifier: ^9.16.0 version: 9.16.0 @@ -55,6 +58,12 @@ importers: version: 8.17.0(eslint@9.16.0)(typescript@5.7.2) packages: + "@babel/runtime@7.26.0": + resolution: { + integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==, + } + engines: { node: ">=6.9.0" } + "@codemirror/autocomplete@6.18.3": resolution: { integrity: sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==, @@ -90,14 +99,14 @@ packages: integrity: sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==, } - "@codemirror/state@6.4.1": + "@codemirror/state@6.5.0": resolution: { - integrity: sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==, + integrity: sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==, } - "@codemirror/view@6.35.0": + "@codemirror/view@6.35.3": resolution: { - integrity: sha512-I0tYy63q5XkaWsJ8QRv5h6ves7kvtrBWjBcnf/bzohFJQc5c14a1AQRdE8QpPF9eMp5Mq2FMm59TCj1gDfE7kw==, + integrity: sha512-ScY7L8+EGdPl4QtoBiOzE4FELp7JmNUsBvgBcCakXWM2uiv/K89VAzU3BMDscf0DsACLvTKePbd5+cFDTcei6g==, } "@dprint/darwin-arm64@0.47.6": @@ -177,15 +186,15 @@ packages: } engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } - "@eslint/config-array@0.19.0": + "@eslint/config-array@0.19.1": resolution: { - integrity: sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==, + integrity: sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - "@eslint/core@0.9.0": + "@eslint/core@0.9.1": resolution: { - integrity: sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==, + integrity: sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } @@ -201,15 +210,15 @@ packages: } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - "@eslint/object-schema@2.1.4": + "@eslint/object-schema@2.1.5": resolution: { - integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==, + integrity: sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - "@eslint/plugin-kit@0.2.3": + "@eslint/plugin-kit@0.2.4": resolution: { - integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==, + integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } @@ -255,6 +264,12 @@ packages: } engines: { node: ">=18.18" } + "@isaacs/cliui@8.0.2": + resolution: { + integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==, + } + engines: { node: ">=12" } + "@lezer/common@1.2.3": resolution: { integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==, @@ -270,6 +285,11 @@ packages: integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==, } + "@marijn/find-cluster-break@1.0.1": + resolution: { + integrity: sha512-7fYyBEBOve5UILdtTr5GnfObe5Jmi8wKwooZ6da1zCr5HZAqweDqvG4ZryVRBjfUDQr2fS8VCnBMiSCX77qt9A==, + } + "@napi-rs/nice-android-arm-eabi@1.0.1": resolution: { integrity: sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==, @@ -422,6 +442,17 @@ packages: } engines: { node: ">= 8" } + "@one-ini/wasm@0.1.1": + resolution: { + integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==, + } + + "@pkgjs/parseargs@0.11.0": + resolution: { + integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==, + } + engines: { node: ">=14" } + "@popperjs/core@2.11.8": resolution: { integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==, @@ -451,89 +482,89 @@ packages: chokidar: optional: true - "@swc/core-darwin-arm64@1.9.3": + "@swc/core-darwin-arm64@1.10.1": resolution: { - integrity: sha512-hGfl/KTic/QY4tB9DkTbNuxy5cV4IeejpPD4zo+Lzt4iLlDWIeANL4Fkg67FiVceNJboqg48CUX+APhDHO5G1w==, + integrity: sha512-NyELPp8EsVZtxH/mEqvzSyWpfPJ1lugpTQcSlMduZLj1EASLO4sC8wt8hmL1aizRlsbjCX+r0PyL+l0xQ64/6Q==, } engines: { node: ">=10" } cpu: [arm64] os: [darwin] - "@swc/core-darwin-x64@1.9.3": + "@swc/core-darwin-x64@1.10.1": resolution: { - integrity: sha512-IaRq05ZLdtgF5h9CzlcgaNHyg4VXuiStnOFpfNEMuI5fm5afP2S0FHq8WdakUz5WppsbddTdplL+vpeApt/WCQ==, + integrity: sha512-L4BNt1fdQ5ZZhAk5qoDfUnXRabDOXKnXBxMDJ+PWLSxOGBbWE6aJTnu4zbGjJvtot0KM46m2LPAPY8ttknqaZA==, } engines: { node: ">=10" } cpu: [x64] os: [darwin] - "@swc/core-linux-arm-gnueabihf@1.9.3": + "@swc/core-linux-arm-gnueabihf@1.10.1": resolution: { - integrity: sha512-Pbwe7xYprj/nEnZrNBvZfjnTxlBIcfApAGdz2EROhjpPj+FBqBa3wOogqbsuGGBdCphf8S+KPprL1z+oDWkmSQ==, + integrity: sha512-Y1u9OqCHgvVp2tYQAJ7hcU9qO5brDMIrA5R31rwWQIAKDkJKtv3IlTHF0hrbWk1wPR0ZdngkQSJZple7G+Grvw==, } engines: { node: ">=10" } cpu: [arm] os: [linux] - "@swc/core-linux-arm64-gnu@1.9.3": + "@swc/core-linux-arm64-gnu@1.10.1": resolution: { - integrity: sha512-AQ5JZiwNGVV/2K2TVulg0mw/3LYfqpjZO6jDPtR2evNbk9Yt57YsVzS+3vHSlUBQDRV9/jqMuZYVU3P13xrk+g==, + integrity: sha512-tNQHO/UKdtnqjc7o04iRXng1wTUXPgVd8Y6LI4qIbHVoVPwksZydISjMcilKNLKIwOoUQAkxyJ16SlOAeADzhQ==, } engines: { node: ">=10" } cpu: [arm64] os: [linux] - "@swc/core-linux-arm64-musl@1.9.3": + "@swc/core-linux-arm64-musl@1.10.1": resolution: { - integrity: sha512-tzVH480RY6RbMl/QRgh5HK3zn1ZTFsThuxDGo6Iuk1MdwIbdFYUY034heWUTI4u3Db97ArKh0hNL0xhO3+PZdg==, + integrity: sha512-x0L2Pd9weQ6n8dI1z1Isq00VHFvpBClwQJvrt3NHzmR+1wCT/gcYl1tp9P5xHh3ldM8Cn4UjWCw+7PaUgg8FcQ==, } engines: { node: ">=10" } cpu: [arm64] os: [linux] - "@swc/core-linux-x64-gnu@1.9.3": + "@swc/core-linux-x64-gnu@1.10.1": resolution: { - integrity: sha512-ivXXBRDXDc9k4cdv10R21ccBmGebVOwKXT/UdH1PhxUn9m/h8erAWjz5pcELwjiMf27WokqPgaWVfaclDbgE+w==, + integrity: sha512-yyYEwQcObV3AUsC79rSzN9z6kiWxKAVJ6Ntwq2N9YoZqSPYph+4/Am5fM1xEQYf/kb99csj0FgOelomJSobxQA==, } engines: { node: ">=10" } cpu: [x64] os: [linux] - "@swc/core-linux-x64-musl@1.9.3": + "@swc/core-linux-x64-musl@1.10.1": resolution: { - integrity: sha512-ILsGMgfnOz1HwdDz+ZgEuomIwkP1PHT6maigZxaCIuC6OPEhKE8uYna22uU63XvYcLQvZYDzpR3ms47WQPuNEg==, + integrity: sha512-tcaS43Ydd7Fk7sW5ROpaf2Kq1zR+sI5K0RM+0qYLYYurvsJruj3GhBCaiN3gkzd8m/8wkqNqtVklWaQYSDsyqA==, } engines: { node: ">=10" } cpu: [x64] os: [linux] - "@swc/core-win32-arm64-msvc@1.9.3": + "@swc/core-win32-arm64-msvc@1.10.1": resolution: { - integrity: sha512-e+XmltDVIHieUnNJHtspn6B+PCcFOMYXNJB1GqoCcyinkEIQNwC8KtWgMqUucUbEWJkPc35NHy9k8aCXRmw9Kg==, + integrity: sha512-D3Qo1voA7AkbOzQ2UGuKNHfYGKL6eejN8VWOoQYtGHHQi1p5KK/Q7V1ku55oxXBsj79Ny5FRMqiRJpVGad7bjQ==, } engines: { node: ">=10" } cpu: [arm64] os: [win32] - "@swc/core-win32-ia32-msvc@1.9.3": + "@swc/core-win32-ia32-msvc@1.10.1": resolution: { - integrity: sha512-rqpzNfpAooSL4UfQnHhkW8aL+oyjqJniDP0qwZfGnjDoJSbtPysHg2LpcOBEdSnEH+uIZq6J96qf0ZFD8AGfXA==, + integrity: sha512-WalYdFoU3454Og+sDKHM1MrjvxUGwA2oralknXkXL8S0I/8RkWZOB++p3pLaGbTvOO++T+6znFbQdR8KRaa7DA==, } engines: { node: ">=10" } cpu: [ia32] os: [win32] - "@swc/core-win32-x64-msvc@1.9.3": + "@swc/core-win32-x64-msvc@1.10.1": resolution: { - integrity: sha512-3YJJLQ5suIEHEKc1GHtqVq475guiyqisKSoUnoaRtxkDaW5g1yvPt9IoSLOe2mRs7+FFhGGU693RsBUSwOXSdQ==, + integrity: sha512-JWobfQDbTnoqaIwPKQ3DVSywihVXlQMbDuwik/dDWlj33A8oEHcjPOGs4OqcA3RHv24i+lfCQpM3Mn4FAMfacA==, } engines: { node: ">=10" } cpu: [x64] os: [win32] - "@swc/core@1.9.3": + "@swc/core@1.10.1": resolution: { - integrity: sha512-oRj0AFePUhtatX+BscVhnzaAmWjpfAeySpM1TCbxA1rtBDeH/JDhi5yYzAKneDYtVtBvA7ApfeuzhMC9ye4xSg==, + integrity: sha512-rQ4dS6GAdmtzKiCRt3LFVxl37FaY1cgL9kSUTnhQ2xc3fmHOd7jdJK/V4pSZMG1ruGTd0bsi34O2R0Olg9Zo/w==, } engines: { node: ">=10" } peerDependencies: @@ -734,6 +765,12 @@ packages: } engines: { node: ^14.14.0 || >=16.0.0 } + abbrev@2.0.0: + resolution: { + integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==, + } + engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 } + acorn-jsx@5.3.2: resolution: { integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, @@ -760,12 +797,42 @@ packages: integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==, } + ansi-colors@4.1.3: + resolution: { + integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==, + } + engines: { node: ">=6" } + + ansi-regex@5.0.1: + resolution: { + integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, + } + engines: { node: ">=8" } + + ansi-regex@6.1.0: + resolution: { + integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==, + } + engines: { node: ">=12" } + ansi-styles@4.3.0: resolution: { integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, } engines: { node: ">=8" } + ansi-styles@6.2.1: + resolution: { + integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==, + } + engines: { node: ">=12" } + + anymatch@3.1.3: + resolution: { + integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==, + } + engines: { node: ">= 8" } + arch@3.0.0: resolution: { integrity: sha512-AmIAC+Wtm2AU8lGfTtHsw0Y9Qtftx2YXEEtiBP10xFUtMOA+sHHx6OAddyL52mUKh1vsXQ6/w1mVDptZCyUt4Q==, @@ -813,6 +880,17 @@ packages: } engines: { node: ">=12" } + binary-extensions@2.3.0: + resolution: { + integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==, + } + engines: { node: ">=8" } + + boolbase@1.0.0: + resolution: { + integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==, + } + bootstrap@5.3.3: resolution: { integrity: sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==, @@ -864,12 +942,46 @@ packages: } engines: { node: ">=6" } + camel-case@3.0.0: + resolution: { + integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==, + } + chalk@4.1.2: resolution: { integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, } engines: { node: ">=10" } + cheerio-select@2.1.0: + resolution: { + integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==, + } + + cheerio@1.0.0-rc.12: + resolution: { + integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==, + } + engines: { node: ">= 6" } + + chokidar@3.6.0: + resolution: { + integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==, + } + engines: { node: ">= 8.10.0" } + + clean-css@4.2.4: + resolution: { + integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==, + } + engines: { node: ">= 4.0" } + + cliui@8.0.1: + resolution: { + integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==, + } + engines: { node: ">=12" } + codemirror@6.0.1: resolution: { integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==, @@ -886,6 +998,17 @@ packages: integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, } + commander@10.0.1: + resolution: { + integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==, + } + engines: { node: ">=14" } + + commander@2.20.3: + resolution: { + integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==, + } + commander@6.2.1: resolution: { integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==, @@ -903,6 +1026,11 @@ packages: integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, } + config-chain@1.1.13: + resolution: { + integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==, + } + content-disposition@0.5.4: resolution: { integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==, @@ -920,9 +1048,20 @@ packages: } engines: { node: ">= 8" } - debug@4.3.7: + css-select@5.1.0: + resolution: { + integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==, + } + + css-what@6.1.0: + resolution: { + integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==, + } + engines: { node: ">= 6" } + + debug@4.4.0: resolution: { - integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==, + integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==, } engines: { node: ">=6.0" } peerDependencies: @@ -954,18 +1093,111 @@ packages: } engines: { node: ">=10" } + detect-node@2.1.0: + resolution: { + integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==, + } + + dom-serializer@1.4.1: + resolution: { + integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==, + } + + dom-serializer@2.0.0: + resolution: { + integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==, + } + + domelementtype@2.3.0: + resolution: { + integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==, + } + + domhandler@3.3.0: + resolution: { + integrity: sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==, + } + engines: { node: ">= 4" } + + domhandler@4.3.1: + resolution: { + integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==, + } + engines: { node: ">= 4" } + + domhandler@5.0.3: + resolution: { + integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==, + } + engines: { node: ">= 4" } + + domutils@2.8.0: + resolution: { + integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==, + } + + domutils@3.1.0: + resolution: { + integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==, + } + dprint@0.47.6: resolution: { integrity: sha512-vCQC+IMHVZbISA5pxEj+yshQbozmQoVFA4lzcLlqJ8rzIAH8U+1DKvesN/2Uv3Bqz6rMW6W4WY7pYJQljmiZ8w==, } hasBin: true + eastasianwidth@0.2.0: + resolution: { + integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==, + } + + editorconfig@1.0.4: + resolution: { + integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==, + } + engines: { node: ">=14" } + hasBin: true + + emoji-regex@8.0.0: + resolution: { + integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==, + } + + emoji-regex@9.2.2: + resolution: { + integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==, + } + emojis-list@3.0.0: resolution: { integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==, } engines: { node: ">= 4" } + entities@2.2.0: + resolution: { + integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==, + } + + entities@4.5.0: + resolution: { + integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==, + } + engines: { node: ">=0.12" } + + escalade@3.2.0: + resolution: { + integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, + } + engines: { node: ">=6" } + + escape-goat@3.0.0: + resolution: { + integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==, + } + engines: { node: ">=10" } + escape-string-regexp@4.0.0: resolution: { integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, @@ -1134,12 +1366,31 @@ packages: integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==, } + foreground-child@3.3.0: + resolution: { + integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==, + } + engines: { node: ">=14" } + form-data-encoder@2.1.4: resolution: { integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==, } engines: { node: ">= 14.17" } + fsevents@2.3.3: + resolution: { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + os: [darwin] + + get-caller-file@2.0.5: + resolution: { + integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==, + } + engines: { node: 6.* || 8.* || >= 10.* } + get-stream@6.0.1: resolution: { integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==, @@ -1164,6 +1415,12 @@ packages: } engines: { node: ">=10.13.0" } + glob@10.4.5: + resolution: { + integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==, + } + hasBin: true + globals@14.0.0: resolution: { integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==, @@ -1198,6 +1455,34 @@ packages: } engines: { node: ">=8" } + he@1.2.0: + resolution: { + integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==, + } + hasBin: true + + html-minifier@4.0.0: + resolution: { + integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==, + } + engines: { node: ">=6" } + hasBin: true + + htmlparser2@5.0.1: + resolution: { + integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==, + } + + htmlparser2@8.0.2: + resolution: { + integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==, + } + + htmlparser2@9.1.0: + resolution: { + integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==, + } + http-cache-semantics@4.1.1: resolution: { integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==, @@ -1238,17 +1523,34 @@ packages: } engines: { node: ">=0.8.19" } + ini@1.3.8: + resolution: { + integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==, + } + inspect-with-kind@1.0.5: resolution: { integrity: sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==, } + is-binary-path@2.1.0: + resolution: { + integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==, + } + engines: { node: ">=8" } + is-extglob@2.1.1: resolution: { integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, } engines: { node: ">=0.10.0" } + is-fullwidth-code-point@3.0.0: + resolution: { + integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==, + } + engines: { node: ">=8" } + is-glob@4.0.3: resolution: { integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, @@ -1284,6 +1586,24 @@ packages: integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, } + jackspeak@3.4.3: + resolution: { + integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==, + } + + js-beautify@1.15.1: + resolution: { + integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==, + } + engines: { node: ">=14" } + hasBin: true + + js-cookie@3.0.5: + resolution: { + integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==, + } + engines: { node: ">=14" } + js-yaml@4.1.0: resolution: { integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==, @@ -1312,6 +1632,13 @@ packages: engines: { node: ">=6" } hasBin: true + juice@10.0.1: + resolution: { + integrity: sha512-ZhJT1soxJCkOiO55/mz8yeBKTAJhRzX9WBO+16ZTqNTONnnVlUPyVBIzQ7lDRjaBdTbid+bAnyIon/GM3yp4cA==, + } + engines: { node: ">=10.0.0" } + hasBin: true + keyv@4.5.4: resolution: { integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, @@ -1346,18 +1673,38 @@ packages: integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==, } + lodash@4.17.21: + resolution: { + integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==, + } + + lower-case@1.1.4: + resolution: { + integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==, + } + lowercase-keys@3.0.0: resolution: { integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==, } engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + lru-cache@10.4.3: + resolution: { + integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==, + } + make-dir@4.0.0: resolution: { integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==, } engines: { node: ">=10" } + mensch@0.3.4: + resolution: { + integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==, + } + merge-stream@2.0.0: resolution: { integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==, @@ -1381,6 +1728,13 @@ packages: } engines: { node: ">= 0.6" } + mime@2.6.0: + resolution: { + integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==, + } + engines: { node: ">=4.0.0" } + hasBin: true + mimic-fn@2.1.0: resolution: { integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==, @@ -1404,89 +1758,324 @@ packages: integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==, } + minimatch@9.0.1: + resolution: { + integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==, + } + engines: { node: ">=16 || 14 >=14.17" } + minimatch@9.0.5: resolution: { integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==, } engines: { node: ">=16 || 14 >=14.17" } - ms@2.1.3: + minipass@7.1.2: resolution: { - integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, + integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==, } + engines: { node: ">=16 || 14 >=14.17" } - natural-compare@1.4.0: + mjml-accordion@4.15.3: resolution: { - integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, + integrity: sha512-LPNVSj1LyUVYT9G1gWwSw3GSuDzDsQCu0tPB2uDsq4VesYNnU6v3iLCQidMiR6azmIt13OEozG700ygAUuA6Ng==, } - normalize-url@8.0.1: + mjml-body@4.15.3: resolution: { - integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==, + integrity: sha512-7pfUOVPtmb0wC+oUOn4xBsAw4eT5DyD6xqaxj/kssu6RrFXOXgJaVnDPAI9AzIvXJ/5as9QrqRGYAddehwWpHQ==, } - engines: { node: ">=14.16" } - npm-run-path@4.0.1: + mjml-button@4.15.3: resolution: { - integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==, + integrity: sha512-79qwn9AgdGjJR1vLnrcm2rq2AsAZkKC5JPwffTMG+Nja6zGYpTDZFZ56ekHWr/r1b5WxkukcPj2PdevUug8c+Q==, } - engines: { node: ">=8" } - onetime@5.1.2: + mjml-carousel@4.15.3: resolution: { - integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==, + integrity: sha512-3ju6I4l7uUhPRrJfN3yK9AMsfHvrYbRkcJ1GRphFHzUj37B2J6qJOQUpzA547Y4aeh69TSb7HFVf1t12ejQxVw==, } - engines: { node: ">=6" } - optionator@0.9.4: + mjml-cli@4.15.3: resolution: { - integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, + integrity: sha512-+V2TDw3tXUVEptFvLSerz125C2ogYl8klIBRY1m5BHd4JvGVf3yhx8N3PngByCzA6PGcv/eydGQN+wy34SHf0Q==, } - engines: { node: ">= 0.8.0" } + hasBin: true - p-cancelable@3.0.0: + mjml-column@4.15.3: resolution: { - integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==, + integrity: sha512-hYdEFdJGHPbZJSEysykrevEbB07yhJGSwfDZEYDSbhQQFjV2tXrEgYcFD5EneMaowjb55e3divSJxU4c5q4Qgw==, } - engines: { node: ">=12.20" } - p-limit@3.1.0: + mjml-core@4.15.3: resolution: { - integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, + integrity: sha512-Dmwk+2cgSD9L9GmTbEUNd8QxkTZtW9P7FN/ROZW/fGZD6Hq6/4TB0zEspg2Ow9eYjZXO2ofOJ3PaQEEShKV0kQ==, } - engines: { node: ">=10" } - p-locate@5.0.0: + mjml-divider@4.15.3: resolution: { - integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, + integrity: sha512-vh27LQ9FG/01y0b9ntfqm+GT5AjJnDSDY9hilss2ixIUh0FemvfGRfsGVeV5UBVPBKK7Ffhvfqc7Rciob9Spzw==, } - engines: { node: ">=10" } - parent-module@1.0.1: + mjml-group@4.15.3: resolution: { - integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, + integrity: sha512-HSu/rKnGZVKFq3ciT46vi1EOy+9mkB0HewO4+P6dP/Y0UerWkN6S3UK11Cxsj0cAp0vFwkPDCdOeEzRdpFEkzA==, } - engines: { node: ">=6" } - path-exists@4.0.0: + mjml-head-attributes@4.15.3: resolution: { - integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, + integrity: sha512-2ISo0r5ZKwkrvJgDou9xVPxxtXMaETe2AsAA02L89LnbB2KC0N5myNsHV0sEysTw9+CfCmgjAb0GAI5QGpxKkQ==, } - engines: { node: ">=8" } - path-key@3.1.1: + mjml-head-breakpoint@4.15.3: resolution: { - integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, + integrity: sha512-Eo56FA5C2v6ucmWQL/JBJ2z641pLOom4k0wP6CMZI2utfyiJ+e2Uuinj1KTrgDcEvW4EtU9HrfAqLK9UosLZlg==, } - engines: { node: ">=8" } - peek-readable@5.3.1: + mjml-head-font@4.15.3: resolution: { - integrity: sha512-GVlENSDW6KHaXcd9zkZltB7tCLosKB/4Hg0fqBJkAoBgYG2Tn1xtMgXtSUuMU9AK/gCm/tTdT8mgAeF4YNeeqw==, + integrity: sha512-CzV2aDPpiNIIgGPHNcBhgyedKY4SX3BJoTwOobSwZVIlEA6TAWB4Z9WwFUmQqZOgo1AkkiTHPZQvGcEhFFXH6g==, } - engines: { node: ">=14.16" } - pend@1.2.0: + mjml-head-html-attributes@4.15.3: + resolution: { + integrity: sha512-MDNDPMBOgXUZYdxhosyrA2kudiGO8aogT0/cODyi2Ed9o/1S7W+je11JUYskQbncqhWKGxNyaP4VWa+6+vUC/g==, + } + + mjml-head-preview@4.15.3: + resolution: { + integrity: sha512-J2PxCefUVeFwsAExhrKo4lwxDevc5aKj888HBl/wN4EuWOoOg06iOGCxz4Omd8dqyFsrqvbBuPqRzQ+VycGmaA==, + } + + mjml-head-style@4.15.3: + resolution: { + integrity: sha512-9J+JuH+mKrQU65CaJ4KZegACUgNIlYmWQYx3VOBR/tyz+8kDYX7xBhKJCjQ1I4wj2Tvga3bykd89Oc2kFZ5WOw==, + } + + mjml-head-title@4.15.3: + resolution: { + integrity: sha512-IM59xRtsxID4DubQ0iLmoCGXguEe+9BFG4z6y2xQDrscIa4QY3KlfqgKGT69ojW+AVbXXJPEVqrAi4/eCsLItQ==, + } + + mjml-head@4.15.3: + resolution: { + integrity: sha512-o3mRuuP/MB5fZycjD3KH/uXsnaPl7Oo8GtdbJTKtH1+O/3pz8GzGMkscTKa97l03DAG2EhGrzzLcU2A6eshwFw==, + } + + mjml-hero@4.15.3: + resolution: { + integrity: sha512-9cLAPuc69yiuzNrMZIN58j+HMK1UWPaq2i3/Fg2ZpimfcGFKRcPGCbEVh0v+Pb6/J0+kf8yIO0leH20opu3AyQ==, + } + + mjml-image@4.15.3: + resolution: { + integrity: sha512-g1OhSdofIytE9qaOGdTPmRIp7JsCtgO0zbsn1Fk6wQh2gEL55Z40j/VoghslWAWTgT2OHFdBKnMvWtN6U5+d2Q==, + } + + mjml-migrate@4.15.3: + resolution: { + integrity: sha512-sr/+35RdxZroNQVegjpfRHJ5hda9XCgaS4mK2FGO+Mb1IUevKfeEPII3F/cHDpNwFeYH3kAgyqQ22ClhGLWNBA==, + } + hasBin: true + + mjml-navbar@4.15.3: + resolution: { + integrity: sha512-VsKH/Jdlf8Yu3y7GpzQV5n7JMdpqvZvTSpF6UQXL0PWOm7k6+LX+sCZimOfpHJ+wCaaybpxokjWZ71mxOoCWoA==, + } + + mjml-parser-xml@4.15.3: + resolution: { + integrity: sha512-Tz0UX8/JVYICLjT+U8J1f/TFxIYVYjzZHeh4/Oyta0pLpRLeZlxEd71f3u3kdnulCKMP4i37pFRDmyLXAlEuLw==, + } + + mjml-preset-core@4.15.3: + resolution: { + integrity: sha512-1zZS8P4O0KweWUqNS655+oNnVMPQ1Rq1GaZq5S9JfwT1Vh/m516lSmiTW9oko6gGHytt5s6Yj6oOeu5Zm8FoLw==, + } + + mjml-raw@4.15.3: + resolution: { + integrity: sha512-IGyHheOYyRchBLiAEgw3UM11kFNmBSMupu2BDdejC6ZiDhEAdG+tyERlsCwDPYtXanvFpGWULIu3XlsUPc+RZw==, + } + + mjml-section@4.15.3: + resolution: { + integrity: sha512-JfVPRXH++Hd933gmQfG8JXXCBCR6fIzC3DwiYycvanL/aW1cEQ2EnebUfQkt5QzlYjOkJEH+JpccAsq3ln6FZQ==, + } + + mjml-social@4.15.3: + resolution: { + integrity: sha512-7sD5FXrESOxpT9Z4Oh36bS6u/geuUrMP1aCg2sjyAwbPcF1aWa2k9OcatQfpRf6pJEhUZ18y6/WBBXmMVmSzXg==, + } + + mjml-spacer@4.15.3: + resolution: { + integrity: sha512-3B7Qj+17EgDdAtZ3NAdMyOwLTX1jfmJuY7gjyhS2HtcZAmppW+cxqHUBwCKfvSRgTQiccmEvtNxaQK+tfyrZqA==, + } + + mjml-table@4.15.3: + resolution: { + integrity: sha512-FLx7DcRKTdKdcOCbMyBaeudeHaHpwPveRrBm6WyQe3LXx6FfdmOh59i71/16LFQMgBOD3N4/UJkzxLzlTJzMqQ==, + } + + mjml-text@4.15.3: + resolution: { + integrity: sha512-+C0hxCmw9kg0XzT6vhE5mFkK6y225nC8UEQcN94K0fBCjPKkM+HqZMwGX205fzdGRi+Bxa55b/VhrIVwdv+8vw==, + } + + mjml-validator@4.15.3: + resolution: { + integrity: sha512-Xb72KdqRwjv/qM2rJpV22syyP2N3cRQ9VVDrN6u2FSzLq02buFNxmSPJ7CKhat3PrUNdVHU75KZwOf/tz4UEhA==, + } + + mjml-wrapper@4.15.3: + resolution: { + integrity: sha512-ditsCijeHJrmBmObtJmQ18ddLxv5oPyMTdPU8Di8APOnD2zPk7Z4UAuJSl7HXB45oFiivr3MJf4koFzMUSZ6Gg==, + } + + mjml@4.15.3: + resolution: { + integrity: sha512-bW2WpJxm6HS+S3Yu6tq1DUPFoTxU9sPviUSmnL7Ua+oVO3WA5ILFWqvujUlz+oeuM+HCwEyMiP5xvKNPENVjYA==, + } + hasBin: true + + ms@2.1.3: + resolution: { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, + } + + natural-compare@1.4.0: + resolution: { + integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, + } + + no-case@2.3.2: + resolution: { + integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==, + } + + node-fetch@2.7.0: + resolution: { + integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==, + } + engines: { node: 4.x || >=6.0.0 } + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + nopt@7.2.1: + resolution: { + integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==, + } + engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 } + hasBin: true + + normalize-path@3.0.0: + resolution: { + integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==, + } + engines: { node: ">=0.10.0" } + + normalize-url@8.0.1: + resolution: { + integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==, + } + engines: { node: ">=14.16" } + + npm-run-path@4.0.1: + resolution: { + integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==, + } + engines: { node: ">=8" } + + nth-check@2.1.1: + resolution: { + integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==, + } + + onetime@5.1.2: + resolution: { + integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==, + } + engines: { node: ">=6" } + + optionator@0.9.4: + resolution: { + integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, + } + engines: { node: ">= 0.8.0" } + + p-cancelable@3.0.0: + resolution: { + integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==, + } + engines: { node: ">=12.20" } + + p-limit@3.1.0: + resolution: { + integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, + } + engines: { node: ">=10" } + + p-locate@5.0.0: + resolution: { + integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, + } + engines: { node: ">=10" } + + package-json-from-dist@1.0.1: + resolution: { + integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==, + } + + param-case@2.1.1: + resolution: { + integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==, + } + + parent-module@1.0.1: + resolution: { + integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, + } + engines: { node: ">=6" } + + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: { + integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==, + } + + parse5@7.2.1: + resolution: { + integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==, + } + + path-exists@4.0.0: + resolution: { + integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, + } + engines: { node: ">=8" } + + path-key@3.1.1: + resolution: { + integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, + } + engines: { node: ">=8" } + + path-scurry@1.11.1: + resolution: { + integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==, + } + engines: { node: ">=16 || 14 >=14.18" } + + peek-readable@5.3.1: + resolution: { + integrity: sha512-GVlENSDW6KHaXcd9zkZltB7tCLosKB/4Hg0fqBJkAoBgYG2Tn1xtMgXtSUuMU9AK/gCm/tTdT8mgAeF4YNeeqw==, + } + engines: { node: ">=14.16" } + + pend@1.2.0: resolution: { integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==, } @@ -1497,9 +2086,9 @@ packages: } engines: { node: ">=8.6" } - piscina@4.7.0: + piscina@4.8.0: resolution: { - integrity: sha512-b8hvkpp9zS0zsfa939b/jXbe64Z2gZv0Ha7FYPNUiDIB1y2AtxcOZdfP8xN8HFjUaqQiT9gRlfjAsoL8vdJ1Iw==, + integrity: sha512-EZJb+ZxDrQf3dihsUL7p42pjNyrNIFJCrRHPMgxu/svsj+P3xS3fuEWp7k2+rfsavfl1N0G29b1HGs7J0m8rZA==, } prelude-ls@1.2.1: @@ -1508,6 +2097,11 @@ packages: } engines: { node: ">= 0.8.0" } + proto-list@1.2.4: + resolution: { + integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==, + } + punycode@2.3.1: resolution: { integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, @@ -1530,6 +2124,29 @@ packages: } engines: { node: ">=10" } + readdirp@3.6.0: + resolution: { + integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==, + } + engines: { node: ">=8.10.0" } + + regenerator-runtime@0.14.1: + resolution: { + integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==, + } + + relateurl@0.2.7: + resolution: { + integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==, + } + engines: { node: ">= 0.10" } + + require-directory@2.1.1: + resolution: { + integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==, + } + engines: { node: ">=0.10.0" } + resolve-alpn@1.2.1: resolution: { integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==, @@ -1611,12 +2228,23 @@ packages: integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==, } + signal-exit@4.1.0: + resolution: { + integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==, + } + engines: { node: ">=14" } + slash@3.0.0: resolution: { integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==, } engines: { node: ">=8" } + slick@1.12.2: + resolution: { + integrity: sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==, + } + sort-keys-length@1.0.1: resolution: { integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==, @@ -1629,17 +2257,47 @@ packages: } engines: { node: ">=0.10.0" } + source-map@0.6.1: + resolution: { + integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==, + } + engines: { node: ">=0.10.0" } + source-map@0.7.4: resolution: { integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==, } engines: { node: ">= 8" } - streamx@2.20.2: + streamx@2.21.0: resolution: { - integrity: sha512-aDGDLU+j9tJcUdPGOaHmVF1u/hhI+CsGkT02V3OKlHDV7IukOI+nTWAGkiZEKCO35rWN1wIr4tS7YFr1f4qSvA==, + integrity: sha512-Qz6MsDZXJ6ur9u+b+4xCG18TluU7PGlRfXVAAjNiGsFrBUt/ioyLkxbFaKJygoPs+/kW4VyBj0bSj89Qu0IGyg==, } + string-width@4.2.3: + resolution: { + integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==, + } + engines: { node: ">=8" } + + string-width@5.1.2: + resolution: { + integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==, + } + engines: { node: ">=12" } + + strip-ansi@6.0.1: + resolution: { + integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, + } + engines: { node: ">=8" } + + strip-ansi@7.1.0: + resolution: { + integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==, + } + engines: { node: ">=12" } + strip-dirs@3.0.0: resolution: { integrity: sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ==, @@ -1679,9 +2337,9 @@ packages: integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==, } - text-decoder@1.2.1: + text-decoder@1.2.2: resolution: { - integrity: sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==, + integrity: sha512-/MDslo7ZyWTA2vnk1j7XoDVfXsGk3tp+zFEJHJGm0UjIlQifonVFwlVbQDFh8KJzTBnT8ie115TYqir6bclddA==, } through@2.3.8: @@ -1701,6 +2359,11 @@ packages: } engines: { node: ">=14.16" } + tr46@0.0.3: + resolution: { + integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==, + } + ts-api-utils@1.4.3: resolution: { integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==, @@ -1734,6 +2397,13 @@ packages: engines: { node: ">=14.17" } hasBin: true + uglify-js@3.19.3: + resolution: { + integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==, + } + engines: { node: ">=0.8.0" } + hasBin: true + uint8array-extras@1.4.0: resolution: { integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==, @@ -1745,16 +2415,43 @@ packages: integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==, } + upper-case@1.1.3: + resolution: { + integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==, + } + uri-js@4.4.1: resolution: { integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, } + valid-data-url@3.0.1: + resolution: { + integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==, + } + engines: { node: ">=10" } + w3c-keyname@2.2.8: resolution: { integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==, } + web-resource-inliner@6.0.1: + resolution: { + integrity: sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==, + } + engines: { node: ">=10.0.0" } + + webidl-conversions@3.0.1: + resolution: { + integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==, + } + + whatwg-url@5.0.0: + resolution: { + integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==, + } + which@2.0.2: resolution: { integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, @@ -1768,6 +2465,36 @@ packages: } engines: { node: ">=0.10.0" } + wrap-ansi@7.0.0: + resolution: { + integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==, + } + engines: { node: ">=10" } + + wrap-ansi@8.1.0: + resolution: { + integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==, + } + engines: { node: ">=12" } + + y18n@5.0.8: + resolution: { + integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==, + } + engines: { node: ">=10" } + + yargs-parser@21.1.1: + resolution: { + integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==, + } + engines: { node: ">=12" } + + yargs@17.7.2: + resolution: { + integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==, + } + engines: { node: ">=12" } + yauzl@3.2.0: resolution: { integrity: sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==, @@ -1781,25 +2508,29 @@ packages: engines: { node: ">=10" } snapshots: - "@codemirror/autocomplete@6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)": + "@babel/runtime@7.26.0": + dependencies: + regenerator-runtime: 0.14.1 + + "@codemirror/autocomplete@6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3)": dependencies: "@codemirror/language": 6.10.6 - "@codemirror/state": 6.4.1 - "@codemirror/view": 6.35.0 + "@codemirror/state": 6.5.0 + "@codemirror/view": 6.35.3 "@lezer/common": 1.2.3 "@codemirror/commands@6.7.1": dependencies: "@codemirror/language": 6.10.6 - "@codemirror/state": 6.4.1 - "@codemirror/view": 6.35.0 + "@codemirror/state": 6.5.0 + "@codemirror/view": 6.35.3 "@lezer/common": 1.2.3 - "@codemirror/lang-sql@6.8.0(@codemirror/view@6.35.0)": + "@codemirror/lang-sql@6.8.0(@codemirror/view@6.35.3)": dependencies: - "@codemirror/autocomplete": 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) + "@codemirror/autocomplete": 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3) "@codemirror/language": 6.10.6 - "@codemirror/state": 6.4.1 + "@codemirror/state": 6.5.0 "@lezer/common": 1.2.3 "@lezer/highlight": 1.2.1 "@lezer/lr": 1.4.2 @@ -1808,8 +2539,8 @@ snapshots: "@codemirror/language@6.10.6": dependencies: - "@codemirror/state": 6.4.1 - "@codemirror/view": 6.35.0 + "@codemirror/state": 6.5.0 + "@codemirror/view": 6.35.3 "@lezer/common": 1.2.3 "@lezer/highlight": 1.2.1 "@lezer/lr": 1.4.2 @@ -1817,21 +2548,23 @@ snapshots: "@codemirror/lint@6.8.4": dependencies: - "@codemirror/state": 6.4.1 - "@codemirror/view": 6.35.0 + "@codemirror/state": 6.5.0 + "@codemirror/view": 6.35.3 crelt: 1.0.6 "@codemirror/search@6.5.8": dependencies: - "@codemirror/state": 6.4.1 - "@codemirror/view": 6.35.0 + "@codemirror/state": 6.5.0 + "@codemirror/view": 6.35.3 crelt: 1.0.6 - "@codemirror/state@6.4.1": {} + "@codemirror/state@6.5.0": + dependencies: + "@marijn/find-cluster-break": 1.0.1 - "@codemirror/view@6.35.0": + "@codemirror/view@6.35.3": dependencies: - "@codemirror/state": 6.4.1 + "@codemirror/state": 6.5.0 style-mod: 4.1.2 w3c-keyname: 2.2.8 @@ -1869,20 +2602,22 @@ snapshots: "@eslint-community/regexpp@4.12.1": {} - "@eslint/config-array@0.19.0": + "@eslint/config-array@0.19.1": dependencies: - "@eslint/object-schema": 2.1.4 - debug: 4.3.7 + "@eslint/object-schema": 2.1.5 + debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - "@eslint/core@0.9.0": {} + "@eslint/core@0.9.1": + dependencies: + "@types/json-schema": 7.0.15 "@eslint/eslintrc@3.2.0": dependencies: ajv: 6.12.6 - debug: 4.3.7 + debug: 4.4.0 espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -1895,9 +2630,9 @@ snapshots: "@eslint/js@9.16.0": {} - "@eslint/object-schema@2.1.4": {} + "@eslint/object-schema@2.1.5": {} - "@eslint/plugin-kit@0.2.3": + "@eslint/plugin-kit@0.2.4": dependencies: levn: 0.4.1 @@ -1920,6 +2655,15 @@ snapshots: "@humanwhocodes/retry@0.4.1": {} + "@isaacs/cliui@8.0.2": + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + "@lezer/common@1.2.3": {} "@lezer/highlight@1.2.1": @@ -1930,6 +2674,8 @@ snapshots: dependencies: "@lezer/common": 1.2.3 + "@marijn/find-cluster-break@1.0.1": {} + "@napi-rs/nice-android-arm-eabi@1.0.1": optional: true @@ -2010,70 +2756,77 @@ snapshots: "@nodelib/fs.scandir": 2.1.5 fastq: 1.17.1 + "@one-ini/wasm@0.1.1": {} + + "@pkgjs/parseargs@0.11.0": + optional: true + "@popperjs/core@2.11.8": {} "@sec-ant/readable-stream@0.4.1": {} "@sindresorhus/is@5.6.0": {} - "@swc/cli@0.5.2(@swc/core@1.9.3)": + "@swc/cli@0.5.2(@swc/core@1.10.1)(chokidar@3.6.0)": dependencies: - "@swc/core": 1.9.3 + "@swc/core": 1.10.1 "@swc/counter": 0.1.3 "@xhmikosr/bin-wrapper": 13.0.5 commander: 8.3.0 fast-glob: 3.3.2 minimatch: 9.0.5 - piscina: 4.7.0 + piscina: 4.8.0 semver: 7.6.3 slash: 3.0.0 source-map: 0.7.4 + optionalDependencies: + chokidar: 3.6.0 - "@swc/core-darwin-arm64@1.9.3": + "@swc/core-darwin-arm64@1.10.1": optional: true - "@swc/core-darwin-x64@1.9.3": + "@swc/core-darwin-x64@1.10.1": optional: true - "@swc/core-linux-arm-gnueabihf@1.9.3": + "@swc/core-linux-arm-gnueabihf@1.10.1": optional: true - "@swc/core-linux-arm64-gnu@1.9.3": + "@swc/core-linux-arm64-gnu@1.10.1": optional: true - "@swc/core-linux-arm64-musl@1.9.3": + "@swc/core-linux-arm64-musl@1.10.1": optional: true - "@swc/core-linux-x64-gnu@1.9.3": + "@swc/core-linux-x64-gnu@1.10.1": optional: true - "@swc/core-linux-x64-musl@1.9.3": + "@swc/core-linux-x64-musl@1.10.1": optional: true - "@swc/core-win32-arm64-msvc@1.9.3": + "@swc/core-win32-arm64-msvc@1.10.1": optional: true - "@swc/core-win32-ia32-msvc@1.9.3": + "@swc/core-win32-ia32-msvc@1.10.1": optional: true - "@swc/core-win32-x64-msvc@1.9.3": + "@swc/core-win32-x64-msvc@1.10.1": optional: true - "@swc/core@1.9.3": + "@swc/core@1.10.1": dependencies: "@swc/counter": 0.1.3 "@swc/types": 0.1.17 optionalDependencies: - "@swc/core-darwin-arm64": 1.9.3 - "@swc/core-darwin-x64": 1.9.3 - "@swc/core-linux-arm-gnueabihf": 1.9.3 - "@swc/core-linux-arm64-gnu": 1.9.3 - "@swc/core-linux-arm64-musl": 1.9.3 - "@swc/core-linux-x64-gnu": 1.9.3 - "@swc/core-linux-x64-musl": 1.9.3 - "@swc/core-win32-arm64-msvc": 1.9.3 - "@swc/core-win32-ia32-msvc": 1.9.3 - "@swc/core-win32-x64-msvc": 1.9.3 + "@swc/core-darwin-arm64": 1.10.1 + "@swc/core-darwin-x64": 1.10.1 + "@swc/core-linux-arm-gnueabihf": 1.10.1 + "@swc/core-linux-arm64-gnu": 1.10.1 + "@swc/core-linux-arm64-musl": 1.10.1 + "@swc/core-linux-x64-gnu": 1.10.1 + "@swc/core-linux-x64-musl": 1.10.1 + "@swc/core-win32-arm64-msvc": 1.10.1 + "@swc/core-win32-ia32-msvc": 1.10.1 + "@swc/core-win32-x64-msvc": 1.10.1 "@swc/counter@0.1.3": {} @@ -2132,7 +2885,7 @@ snapshots: "@typescript-eslint/types": 8.17.0 "@typescript-eslint/typescript-estree": 8.17.0(typescript@5.7.2) "@typescript-eslint/visitor-keys": 8.17.0 - debug: 4.3.7 + debug: 4.4.0 eslint: 9.16.0 optionalDependencies: typescript: 5.7.2 @@ -2148,7 +2901,7 @@ snapshots: dependencies: "@typescript-eslint/typescript-estree": 8.17.0(typescript@5.7.2) "@typescript-eslint/utils": 8.17.0(eslint@9.16.0)(typescript@5.7.2) - debug: 4.3.7 + debug: 4.4.0 eslint: 9.16.0 ts-api-utils: 1.4.3(typescript@5.7.2) optionalDependencies: @@ -2162,7 +2915,7 @@ snapshots: dependencies: "@typescript-eslint/types": 8.17.0 "@typescript-eslint/visitor-keys": 8.17.0 - debug: 4.3.7 + debug: 4.4.0 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -2258,6 +3011,8 @@ snapshots: dependencies: arch: 3.0.0 + abbrev@2.0.0: {} + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 @@ -2275,10 +3030,23 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + arch@3.0.0: {} argparse@2.0.1: {} @@ -2305,6 +3073,10 @@ snapshots: execa: 5.1.1 find-versions: 5.1.0 + binary-extensions@2.3.0: {} + + boolbase@1.0.0: {} + bootstrap@5.3.3(@popperjs/core@2.11.8): dependencies: "@popperjs/core": 2.11.8 @@ -2343,20 +3115,66 @@ snapshots: callsites@3.1.0: {} + camel-case@3.0.0: + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + + cheerio@1.0.0-rc.12: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + htmlparser2: 8.0.2 + parse5: 7.2.1 + parse5-htmlparser2-tree-adapter: 7.1.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + clean-css@4.2.4: + dependencies: + source-map: 0.6.1 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + codemirror@6.0.1(@lezer/common@1.2.3): dependencies: - "@codemirror/autocomplete": 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) + "@codemirror/autocomplete": 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3) "@codemirror/commands": 6.7.1 "@codemirror/language": 6.10.6 "@codemirror/lint": 6.8.4 "@codemirror/search": 6.5.8 - "@codemirror/state": 6.4.1 - "@codemirror/view": 6.35.0 + "@codemirror/state": 6.5.0 + "@codemirror/view": 6.35.3 transitivePeerDependencies: - "@lezer/common" @@ -2366,12 +3184,21 @@ snapshots: color-name@1.1.4: {} + commander@10.0.1: {} + + commander@2.20.3: {} + commander@6.2.1: {} commander@8.3.0: {} concat-map@0.0.1: {} + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -2384,7 +3211,17 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - debug@4.3.7: + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + + css-what@6.1.0: {} + + debug@4.4.0: dependencies: ms: 2.1.3 @@ -2398,6 +3235,46 @@ snapshots: defer-to-connect@2.0.1: {} + detect-node@2.1.0: {} + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@3.3.0: + dependencies: + domelementtype: 2.3.0 + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + domutils@3.1.0: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dprint@0.47.6: optionalDependencies: "@dprint/darwin-arm64": 0.47.6 @@ -2410,8 +3287,29 @@ snapshots: "@dprint/win32-arm64": 0.47.6 "@dprint/win32-x64": 0.47.6 + eastasianwidth@0.2.0: {} + + editorconfig@1.0.4: + dependencies: + "@one-ini/wasm": 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.6.3 + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + emojis-list@3.0.0: {} + entities@2.2.0: {} + + entities@4.5.0: {} + + escalade@3.2.0: {} + + escape-goat@3.0.0: {} + escape-string-regexp@4.0.0: {} eslint-scope@8.2.0: @@ -2427,11 +3325,11 @@ snapshots: dependencies: "@eslint-community/eslint-utils": 4.4.1(eslint@9.16.0) "@eslint-community/regexpp": 4.12.1 - "@eslint/config-array": 0.19.0 - "@eslint/core": 0.9.0 + "@eslint/config-array": 0.19.1 + "@eslint/core": 0.9.1 "@eslint/eslintrc": 3.2.0 "@eslint/js": 9.16.0 - "@eslint/plugin-kit": 0.2.3 + "@eslint/plugin-kit": 0.2.4 "@humanfs/node": 0.16.6 "@humanwhocodes/module-importer": 1.0.1 "@humanwhocodes/retry": 0.4.1 @@ -2440,7 +3338,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.3.7 + debug: 4.4.0 escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 eslint-visitor-keys: 4.2.0 @@ -2558,8 +3456,18 @@ snapshots: flatted@3.3.2: {} + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + form-data-encoder@2.1.4: {} + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + get-stream@6.0.1: {} get-stream@9.0.1: @@ -2575,6 +3483,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + globals@14.0.0: {} globals@15.13.0: {} @@ -2599,6 +3516,39 @@ snapshots: has-flag@4.0.0: {} + he@1.2.0: {} + + html-minifier@4.0.0: + dependencies: + camel-case: 3.0.0 + clean-css: 4.2.4 + commander: 2.20.3 + he: 1.2.0 + param-case: 2.1.1 + relateurl: 0.2.7 + uglify-js: 3.19.3 + + htmlparser2@5.0.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 3.3.0 + domutils: 2.8.0 + entities: 2.2.0 + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + http-cache-semantics@4.1.1: {} http2-wrapper@2.2.1: @@ -2619,12 +3569,20 @@ snapshots: imurmurhash@0.1.4: {} + ini@1.3.8: {} + inspect-with-kind@1.0.5: dependencies: kind-of: 6.0.3 + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -2639,6 +3597,22 @@ snapshots: isexe@2.0.0: {} + jackspeak@3.4.3: + dependencies: + "@isaacs/cliui": 8.0.2 + optionalDependencies: + "@pkgjs/parseargs": 0.11.0 + + js-beautify@1.15.1: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.4.5 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -2651,6 +3625,16 @@ snapshots: json5@2.2.3: {} + juice@10.0.1: + dependencies: + cheerio: 1.0.0-rc.12 + commander: 6.2.1 + mensch: 0.3.4 + slick: 1.12.2 + web-resource-inliner: 6.0.1 + transitivePeerDependencies: + - encoding + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -2674,12 +3658,20 @@ snapshots: lodash.merge@4.6.2: {} + lodash@4.17.21: {} + + lower-case@1.1.4: {} + lowercase-keys@3.0.0: {} + lru-cache@10.4.3: {} + make-dir@4.0.0: dependencies: semver: 7.6.3 + mensch@0.3.4: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -2691,6 +3683,8 @@ snapshots: mime-db@1.53.0: {} + mime@2.6.0: {} + mimic-fn@2.1.0: {} mimic-response@3.1.0: {} @@ -2701,20 +3695,341 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@9.0.1: + dependencies: + brace-expansion: 2.0.1 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 + minipass@7.1.2: {} + + mjml-accordion@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-body@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-button@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-carousel@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-cli@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + chokidar: 3.6.0 + glob: 10.4.5 + html-minifier: 4.0.0 + js-beautify: 1.15.1 + lodash: 4.17.21 + minimatch: 9.0.5 + mjml-core: 4.15.3 + mjml-migrate: 4.15.3 + mjml-parser-xml: 4.15.3 + mjml-validator: 4.15.3 + yargs: 17.7.2 + transitivePeerDependencies: + - encoding + + mjml-column@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-core@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + cheerio: 1.0.0-rc.12 + detect-node: 2.1.0 + html-minifier: 4.0.0 + js-beautify: 1.15.1 + juice: 10.0.1 + lodash: 4.17.21 + mjml-migrate: 4.15.3 + mjml-parser-xml: 4.15.3 + mjml-validator: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-divider@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-group@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-head-attributes@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-head-breakpoint@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-head-font@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-head-html-attributes@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-head-preview@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-head-style@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-head-title@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-head@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-hero@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-image@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-migrate@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + js-beautify: 1.15.1 + lodash: 4.17.21 + mjml-core: 4.15.3 + mjml-parser-xml: 4.15.3 + yargs: 17.7.2 + transitivePeerDependencies: + - encoding + + mjml-navbar@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-parser-xml@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + detect-node: 2.1.0 + htmlparser2: 9.1.0 + lodash: 4.17.21 + + mjml-preset-core@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + mjml-accordion: 4.15.3 + mjml-body: 4.15.3 + mjml-button: 4.15.3 + mjml-carousel: 4.15.3 + mjml-column: 4.15.3 + mjml-divider: 4.15.3 + mjml-group: 4.15.3 + mjml-head: 4.15.3 + mjml-head-attributes: 4.15.3 + mjml-head-breakpoint: 4.15.3 + mjml-head-font: 4.15.3 + mjml-head-html-attributes: 4.15.3 + mjml-head-preview: 4.15.3 + mjml-head-style: 4.15.3 + mjml-head-title: 4.15.3 + mjml-hero: 4.15.3 + mjml-image: 4.15.3 + mjml-navbar: 4.15.3 + mjml-raw: 4.15.3 + mjml-section: 4.15.3 + mjml-social: 4.15.3 + mjml-spacer: 4.15.3 + mjml-table: 4.15.3 + mjml-text: 4.15.3 + mjml-wrapper: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-raw@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-section@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-social@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-spacer@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-table@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-text@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml-validator@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + + mjml-wrapper@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + lodash: 4.17.21 + mjml-core: 4.15.3 + mjml-section: 4.15.3 + transitivePeerDependencies: + - encoding + + mjml@4.15.3: + dependencies: + "@babel/runtime": 7.26.0 + mjml-cli: 4.15.3 + mjml-core: 4.15.3 + mjml-migrate: 4.15.3 + mjml-preset-core: 4.15.3 + mjml-validator: 4.15.3 + transitivePeerDependencies: + - encoding + ms@2.1.3: {} natural-compare@1.4.0: {} + no-case@2.3.2: + dependencies: + lower-case: 1.1.4 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + + normalize-path@3.0.0: {} + normalize-url@8.0.1: {} npm-run-path@4.0.1: dependencies: path-key: 3.1.1 + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -2738,26 +4053,48 @@ snapshots: dependencies: p-limit: 3.1.0 + package-json-from-dist@1.0.1: {} + + param-case@2.1.1: + dependencies: + no-case: 2.3.2 + parent-module@1.0.1: dependencies: callsites: 3.1.0 + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.2.1 + + parse5@7.2.1: + dependencies: + entities: 4.5.0 + path-exists@4.0.0: {} path-key@3.1.1: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + peek-readable@5.3.1: {} pend@1.2.0: {} picomatch@2.3.1: {} - piscina@4.7.0: + piscina@4.8.0: optionalDependencies: "@napi-rs/nice": 1.0.1 prelude-ls@1.2.1: {} + proto-list@1.2.4: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -2766,6 +4103,16 @@ snapshots: quick-lru@5.1.1: {} + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + regenerator-runtime@0.14.1: {} + + relateurl@0.2.7: {} + + require-directory@2.1.1: {} + resolve-alpn@1.2.1: {} resolve-from@4.0.0: {} @@ -2808,8 +4155,12 @@ snapshots: signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + slash@3.0.0: {} + slick@1.12.2: {} + sort-keys-length@1.0.1: dependencies: sort-keys: 1.1.2 @@ -2818,16 +4169,38 @@ snapshots: dependencies: is-plain-obj: 1.1.0 + source-map@0.6.1: {} + source-map@0.7.4: {} - streamx@2.20.2: + streamx@2.21.0: dependencies: fast-fifo: 1.3.2 queue-tick: 1.0.1 - text-decoder: 1.2.1 + text-decoder: 1.2.2 optionalDependencies: bare-events: 2.5.0 + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + strip-dirs@3.0.0: dependencies: inspect-with-kind: 1.0.5 @@ -2852,9 +4225,11 @@ snapshots: dependencies: b4a: 1.6.7 fast-fifo: 1.3.2 - streamx: 2.20.2 + streamx: 2.21.0 - text-decoder@1.2.1: {} + text-decoder@1.2.2: + dependencies: + b4a: 1.6.7 through@2.3.8: {} @@ -2867,6 +4242,8 @@ snapshots: "@tokenizer/token": 0.3.0 ieee754: 1.2.1 + tr46@0.0.3: {} + ts-api-utils@1.4.3(typescript@5.7.2): dependencies: typescript: 5.7.2 @@ -2888,6 +4265,8 @@ snapshots: typescript@5.7.2: {} + uglify-js@3.19.3: {} + uint8array-extras@1.4.0: {} unbzip2-stream@1.4.3: @@ -2895,18 +4274,66 @@ snapshots: buffer: 5.7.1 through: 2.3.8 + upper-case@1.1.3: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 + valid-data-url@3.0.1: {} + w3c-keyname@2.2.8: {} + web-resource-inliner@6.0.1: + dependencies: + ansi-colors: 4.1.3 + escape-goat: 3.0.0 + htmlparser2: 5.0.1 + mime: 2.6.0 + node-fetch: 2.7.0 + valid-data-url: 3.0.1 + transitivePeerDependencies: + - encoding + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 word-wrap@1.2.5: {} + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yauzl@3.2.0: dependencies: buffer-crc32: 0.2.13 diff --git a/src/Command/CompletedQuestionStatCommand.php b/src/Command/CompletedQuestionStatCommand.php new file mode 100644 index 0000000..06599d5 --- /dev/null +++ b/src/Command/CompletedQuestionStatCommand.php @@ -0,0 +1,65 @@ +setHeaderTitle('Solved questions'); + $table->setHeaders(['Email', 'Passed', 'Total', 'Percent']); + + $totalQuestions = $this->questionRepository->count(); + if (0 === $totalQuestions) { + $io->error('No questions found.'); + + return Command::FAILURE; + } + + /** + * @var list}> $solvedQuestions + */ + $solvedQuestions = $this->solutionEventRepository->createQueryBuilder('se') + ->select('u.email', 'COUNT(DISTINCT q) as solved_questions') + ->join('se.question', 'q') + ->join('se.submitter', 'u') + ->where('se.status = :status') + ->groupBy('u.email') + ->orderBy('solved_questions', 'DESC') + ->setParameter('status', SolutionEventStatus::Passed) + ->getQuery() + ->getResult(); + + foreach ($solvedQuestions as $row) { + $solvedQuestions = $row['solved_questions']; + $table->addRow([$row['email'], $solvedQuestions, $totalQuestions, round($solvedQuestions / $totalQuestions * 100, 2).'%']); + } + + $table->render(); + + return Command::SUCCESS; + } +} diff --git a/src/Command/CreateUsersCommand.php b/src/Command/CreateUsersCommand.php index a20489f..04c36c0 100644 --- a/src/Command/CreateUsersCommand.php +++ b/src/Command/CreateUsersCommand.php @@ -151,7 +151,7 @@ private static function parseUsers(string $filename): array $roles = $row[$rolesIndex]; $group = false !== $groupIndex ? $row[$groupIndex] : null; - if (!\is_string($email) || !\is_string($name) || !\is_string($roles) || (!\is_string($group) && null !== $group)) { + if (!\is_string($email) || !\is_string($name) || !\is_string($roles) || !\is_string($group)) { throw new \RuntimeException("Invalid row in $filename."); } diff --git a/src/Command/LastLoginStatCommand.php b/src/Command/LastLoginStatCommand.php new file mode 100644 index 0000000..d946b77 --- /dev/null +++ b/src/Command/LastLoginStatCommand.php @@ -0,0 +1,79 @@ +addOption( + 'moreThan', + 'm', + InputOption::VALUE_OPTIONAL, + 'Only show users who have not logged in for more than this number of days', + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $moreThan = ($moreThan_ = $input->getOption('moreThan')) !== null + ? (int) $moreThan_ + : null; + + /** + * @var list $results + */ + $results = $this->userRepository->createQueryBuilder('user') + ->leftJoin('user.loginEvents', 'loginEvent') + ->select('user.email', 'MAX(loginEvent.createdAt) as last_login_at') + ->groupBy('user.email') + ->orderBy('last_login_at', 'DESC') + ->getQuery() + ->getResult(); + + $table = new Table($output); + $table->setHeaderTitle('Last login date of users'); + $table->setHeaders(['Email', 'Last login', 'Recency']); + foreach ($results as $result) { + $lastLoginAt = ($lastLoginAt = $result['last_login_at']) !== null + ? new \DateTime($lastLoginAt) + : null; + + if (null !== $lastLoginAt) { + $lastLoginAtString = $lastLoginAt->format('Y-m-d H:i:s'); + $recency = $lastLoginAt->diff(new \DateTime()); + + if (null !== $moreThan && $recency->days < $moreThan) { + continue; + } + + $recencyString = $recency->format('%a days %h hours'); + + $table->addRow([$result['email'], $lastLoginAtString, $recencyString]); + } else { + $table->addRow([$result['email'], 'Never logged in', 'N/A']); + } + } + + $table->render(); + + return Command::SUCCESS; + } +} diff --git a/src/Controller/Admin/DashboardController.php b/src/Controller/Admin/DashboardController.php index db432a6..da15f5d 100644 --- a/src/Controller/Admin/DashboardController.php +++ b/src/Controller/Admin/DashboardController.php @@ -7,6 +7,8 @@ use App\Entity\Announcement; use App\Entity\Comment; use App\Entity\CommentLikeEvent; +use App\Entity\Email; +use App\Entity\EmailDeliveryEvent; use App\Entity\Feedback; use App\Entity\Group; use App\Entity\HintOpenEvent; @@ -63,6 +65,11 @@ public function configureMenuItems(): iterable yield MenuItem::linkToCrud('Comment', 'fa fa-comment', Comment::class); yield MenuItem::linkToCrud('CommentLikeEvent', 'fa fa-thumbs-up', CommentLikeEvent::class); + yield MenuItem::section('Mails'); + yield MenuItem::linkToRoute('EmailTemplates', 'fa fa-layer-group', 'app_admin_emailtemplate_index'); + yield MenuItem::linkToCrud('Email', 'fa fa-envelope', Email::class); + yield MenuItem::linkToCrud('EmailDeliveryEvent', 'fa fa-paper-plane', EmailDeliveryEvent::class); + yield MenuItem::section('Events'); yield MenuItem::linkToCrud('SolutionEvent', 'fa fa-check', SolutionEvent::class); yield MenuItem::linkToCrud('SolutionVideoEvent', 'fa fa-video', SolutionVideoEvent::class); diff --git a/src/Controller/Admin/EmailCrudController.php b/src/Controller/Admin/EmailCrudController.php new file mode 100644 index 0000000..4206789 --- /dev/null +++ b/src/Controller/Admin/EmailCrudController.php @@ -0,0 +1,42 @@ +hideOnForm(), + TextField::new('subject'), + CodeEditorField::new('textContent'), + CodeEditorField::new('htmlContent', 'HTML Content')->setLanguage('xml'), + ChoiceField::new('kind'), + ]; + } + + public function configureFilters(Filters $filters): Filters + { + return $filters->add(ChoiceFilter::new('kind')->setTranslatableChoices([ + 'email-kind.transactional' => EmailKind::Transactional, + 'email-kind.marketing' => EmailKind::Marketing, + ])); + } +} diff --git a/src/Controller/Admin/EmailDeliveryEventCrudController.php b/src/Controller/Admin/EmailDeliveryEventCrudController.php new file mode 100644 index 0000000..452406a --- /dev/null +++ b/src/Controller/Admin/EmailDeliveryEventCrudController.php @@ -0,0 +1,62 @@ +hideOnIndex()->setDisabled(), + AssociationField::new('toUser'), + TextField::new('toAddress')->hideOnIndex(), + AssociationField::new('email'), + DateTimeField::new('createdAt', 'Created at')->setDisabled(), + ]; + } + + public function configureFilters(Filters $filters): Filters + { + return $filters + ->add('toUser') + ; + } + + public function configureActions(Actions $actions): Actions + { + $previewAction = Action::new('preview', 'Preview', 'fa fa-eye') + ->linkToUrl(fn (EmailDeliveryEvent $event) => $this->generateUrl( + 'app_email_preview', + ['event' => $event->getId()] + )); + + return $actions + ->disable(Action::DELETE, Action::EDIT, Action::NEW) + ->add(Crud::PAGE_INDEX, Action::DETAIL) + ->add(Crud::PAGE_INDEX, $previewAction) + ->add(Crud::PAGE_DETAIL, $previewAction); + } + + public function configureCrud(Crud $crud): Crud + { + return $crud->setDefaultSort(['createdAt' => 'DESC']); + } +} diff --git a/src/Controller/Admin/EmailTemplateController.php b/src/Controller/Admin/EmailTemplateController.php new file mode 100644 index 0000000..4be03f3 --- /dev/null +++ b/src/Controller/Admin/EmailTemplateController.php @@ -0,0 +1,67 @@ +templateDir = $this->projectDir.'/templates/email/mjml'; + } + + #[Route('/admin/email-template', name: 'app_admin_emailtemplate_index')] + public function index(): Response + { + $templateFiles = glob($this->templateDir.'/*.mjml.twig'); + if (false === $templateFiles) { + throw new \RuntimeException('Failed to list email templates.'); + } + + $templateFiles = array_map( + fn (string $file) => basename($file, '.mjml.twig'), + $templateFiles + ); + + return $this->render('admin/email-template/index.twig', [ + 'templates' => $templateFiles, + ]); + } + + #[Route('/admin/email-template/{name}', name: 'app_admin_emailtemplate_details')] + public function details(string $name, Request $request): Response + { + $parametersJSON = $request->query->get('parameters', '{}'); + $parameters = json_decode($parametersJSON, true); + + if (!\is_array($parameters)) { + throw new \InvalidArgumentException('The parameters must be a valid JSON object.'); + } + + try { + $content = $this->renderView("email/mjml/$name.mjml.twig", $parameters); + $error = null; + } catch (\Throwable $e) { + $content = null; + $error = $e->getMessage(); + } + + return $this->render('admin/email-template/details.twig', [ + 'name' => $name, + 'parameters' => $parameters, + 'content' => $content, + 'error' => $error, + ]); + } +} diff --git a/src/Controller/ChallengeController.php b/src/Controller/ChallengeController.php index 6e3c470..9b83f2d 100644 --- a/src/Controller/ChallengeController.php +++ b/src/Controller/ChallengeController.php @@ -16,7 +16,7 @@ class ChallengeController extends AbstractController { - #[Route('/challenge/{id}', name: 'app_challenge')] + #[Route('/challenge/{question}', name: 'app_challenge')] public function index( #[CurrentUser] User $user, Question $question, @@ -27,7 +27,7 @@ public function index( ]); } - #[Route('/challenge/{id}/solution-video', name: 'app_challenge_solution_video', methods: ['GET'])] + #[Route('/challenge/{question}/solution-video', name: 'app_challenge_solution_video', methods: ['GET'])] public function solution_video( Question $question, EntityManagerInterface $entityManager, diff --git a/src/Controller/CommentsController.php b/src/Controller/CommentsController.php index 8cee2a1..b7ddd9a 100644 --- a/src/Controller/CommentsController.php +++ b/src/Controller/CommentsController.php @@ -99,11 +99,6 @@ public function likes( private function isCommentFeatureEnabled(): bool { - $comment = $this->getParameter('app.features.comment'); - if (!\is_bool($comment)) { - throw new \RuntimeException('The "app.features.comment" parameter must be a boolean.'); - } - - return $comment; + return $this->getParameter('app.features.comment'); } } diff --git a/src/Controller/EmailController.php b/src/Controller/EmailController.php new file mode 100644 index 0000000..dace31f --- /dev/null +++ b/src/Controller/EmailController.php @@ -0,0 +1,29 @@ +getToUser() !== $user && !$this->isGranted('ROLE_ADMIN')) { + throw $this->createAccessDeniedException('You are not authorized to access this email.'); + } + + return $this->render('email/preview.html.twig', [ + 'emailDeliveryEvent' => $event, + ]); + } +} diff --git a/src/Controller/OverviewCardsController.php b/src/Controller/OverviewCardsController.php index 537e2fb..967b963 100644 --- a/src/Controller/OverviewCardsController.php +++ b/src/Controller/OverviewCardsController.php @@ -64,21 +64,12 @@ public function level( $solvedQuestions = $solutionEventRepository->findSolvedQuestions($user); $totalQuestions = $questionRepository->count(); - if (0 === $totalQuestions) { - return $this->render('overview/cards/level.html.twig', [ - 'level' => Level::cases()[0], - 'rawLevelIndex' => 0, - ]); - } - - $solvedQuestionPercent = \count($solvedQuestions) / $totalQuestions; - $levelIndex = ceil(\count(Level::cases()) * $solvedQuestionPercent); - - $level = Level::cases()[$levelIndex]; + $level = \count($solvedQuestions) > 0 + ? Level::fromPercent(\count($solvedQuestions) / $totalQuestions) + : Level::Starter; return $this->render('overview/cards/level.html.twig', [ 'level' => $level, - 'rawLevelIndex' => $levelIndex, ]); } diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php index ffc4b97..a24c34f 100644 --- a/src/Controller/ProfileController.php +++ b/src/Controller/ProfileController.php @@ -21,10 +21,7 @@ class ProfileController extends AbstractController { public function isProfileEditable(): bool { - $isProfileEditable = $this->getParameter('app.features.editable-profile'); - \assert(\is_bool($isProfileEditable)); - - return $isProfileEditable; + return $this->getParameter('app.features.editable-profile'); } #[Route('/profile', name: 'app_profile')] diff --git a/src/Entity/Email.php b/src/Entity/Email.php new file mode 100644 index 0000000..7f7243f --- /dev/null +++ b/src/Entity/Email.php @@ -0,0 +1,135 @@ + + */ + #[ORM\OneToMany(targetEntity: EmailDeliveryEvent::class, mappedBy: 'email')] + private Collection $emailDeliveryEvents; + + #[ORM\Column(enumType: EmailKind::class)] + private EmailKind $kind = EmailKind::Transactional; + + public function __construct() + { + $this->emailDeliveryEvents = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function __toString(): string + { + return $this->getSubject(); + } + + public function getSubject(): string + { + return $this->subject; + } + + public function setSubject(string $subject): static + { + $this->subject = $subject; + + return $this; + } + + public function getTextContent(): string + { + return $this->textContent; + } + + public function setTextContent(string $textContent): static + { + $this->textContent = $textContent; + + return $this; + } + + public function getHtmlContent(): string + { + return $this->htmlContent; + } + + public function setHtmlContent(string $htmlContent): static + { + $this->htmlContent = $htmlContent; + + return $this; + } + + /** + * @return Collection + */ + public function getEmailDeliveryEvents(): Collection + { + return $this->emailDeliveryEvents; + } + + public function addEmailDeliveryEvent(EmailDeliveryEvent $emailDeliveryEvent): static + { + if (!$this->emailDeliveryEvents->contains($emailDeliveryEvent)) { + $this->emailDeliveryEvents->add($emailDeliveryEvent); + $emailDeliveryEvent->setEmail($this); + } + + return $this; + } + + public function removeEmailDeliveryEvent(EmailDeliveryEvent $emailDeliveryEvent): static + { + if ($this->emailDeliveryEvents->removeElement($emailDeliveryEvent)) { + // set the owning side to null (unless already changed) + if ($emailDeliveryEvent->getEmail() === $this) { + $emailDeliveryEvent->setEmail(new self()); + } + } + + return $this; + } + + public function getKind(): EmailKind + { + return $this->kind; + } + + public function setKind(EmailKind $kind): static + { + $this->kind = $kind; + + return $this; + } +} diff --git a/src/Entity/EmailDeliveryEvent.php b/src/Entity/EmailDeliveryEvent.php new file mode 100644 index 0000000..ec6aa73 --- /dev/null +++ b/src/Entity/EmailDeliveryEvent.php @@ -0,0 +1,61 @@ +toUser; + } + + public function setToUser(?User $toUser): static + { + $this->toUser = $toUser; + + return $this; + } + + public function getToAddress(): ?string + { + return $this->toAddress; + } + + public function setToAddress(string $toAddress): static + { + $this->toAddress = $toAddress; + + return $this; + } + + public function getEmail(): Email + { + return $this->email; + } + + public function setEmail(Email $email): static + { + $this->email = $email; + + return $this; + } +} diff --git a/src/Entity/EmailDto/EmailDto.php b/src/Entity/EmailDto/EmailDto.php new file mode 100644 index 0000000..1ec9640 --- /dev/null +++ b/src/Entity/EmailDto/EmailDto.php @@ -0,0 +1,101 @@ +setToAddress(new Address( + address: $user->getEmail(), + name: $user->getName() ?? $user->getEmail(), + )); + } + + public function getToAddress(): Address + { + return $this->toAddress; + } + + public function setToAddress(Address $toAddress): self + { + $this->toAddress = $toAddress; + + return $this; + } + + public function getSubject(): string + { + return $this->subject; + } + + public function setSubject(string $subject): self + { + $this->subject = $subject; + + return $this; + } + + public function getKind(): EmailKind + { + return $this->kind; + } + + public function setKind(EmailKind $kind): self + { + $this->kind = $kind; + + return $this; + } + + public function getText(): string + { + return $this->text; + } + + public function setText(string $text): self + { + $this->text = $text; + + return $this; + } + + public function getHtml(): string + { + return $this->html; + } + + public function setHtml(string $html): self + { + $this->html = $html; + + return $this; + } + + public function toEmail(): Email + { + $email = (new Email()) + ->to($this->getToAddress()) + ->subject($this->getSubject()) + ->text($this->getText()) + ->html($this->getHtml()); + + $headers = $this->getKind()->addToEmailHeader($email->getHeaders()); + + return $email->setHeaders($headers); + } +} diff --git a/src/Entity/EmailKind.php b/src/Entity/EmailKind.php new file mode 100644 index 0000000..de1c3c8 --- /dev/null +++ b/src/Entity/EmailKind.php @@ -0,0 +1,50 @@ + $translator->trans('email-kind.transactional', locale: $locale), + self::Marketing => $translator->trans('email-kind.marketing', locale: $locale), + self::Test => $translator->trans('email-kind.test', locale: $locale), + }; + } + + /** + * @throws \InvalidArgumentException + */ + public static function fromEmailHeader(Headers $headers): self + { + $kind = $headers->getHeaderBody(self::EMAIL_HEADER); + if (!\is_string($kind)) { + throw new \InvalidArgumentException('The email kind header is missing or is invalid type.'); + } + + return match ($kind) { + 'transactional' => self::Transactional, + 'marketing' => self::Marketing, + 'test' => self::Test, + default => throw new \InvalidArgumentException("Invalid email kind: $kind"), + }; + } + + public function addToEmailHeader(Headers $headers): Headers + { + return $headers->addTextHeader(self::EMAIL_HEADER, $this->value); + } +} diff --git a/src/Entity/Level.php b/src/Entity/Level.php index 6db1d97..647ea68 100644 --- a/src/Entity/Level.php +++ b/src/Entity/Level.php @@ -23,4 +23,16 @@ public function trans(TranslatorInterface $translator, ?string $locale = null): { return $translator->trans('level.'.$this->value, locale: $locale); } + + public static function fromPercent(float $percent): self + { + return match (true) { + $percent < 5 => self::Starter, + $percent < 20 => self::Beginner, + $percent < 40 => self::Intermediate, + $percent < 65 => self::Advanced, + $percent < 90 => self::Expert, + default => self::Master, + }; + } } diff --git a/src/Entity/User.php b/src/Entity/User.php index 3d2c1e6..3755ff9 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -90,6 +90,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\OneToMany(targetEntity: Feedback::class, mappedBy: 'sender')] private Collection $feedback; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: EmailDeliveryEvent::class, mappedBy: 'toUser')] + private Collection $emailDeliveryEvents; + public function __construct() { $this->solutionEvents = new ArrayCollection(); @@ -99,6 +105,7 @@ public function __construct() $this->hintOpenEvents = new ArrayCollection(); $this->loginEvents = new ArrayCollection(); $this->feedback = new ArrayCollection(); + $this->emailDeliveryEvents = new ArrayCollection(); } public function getId(): ?int @@ -419,4 +426,34 @@ public function removeFeedback(Feedback $feedback): static return $this; } + + /** + * @return Collection + */ + public function getEmailDeliveryEvents(): Collection + { + return $this->emailDeliveryEvents; + } + + public function addEmailDeliveryEvent(EmailDeliveryEvent $emailDeliveryEvent): static + { + if (!$this->emailDeliveryEvents->contains($emailDeliveryEvent)) { + $this->emailDeliveryEvents->add($emailDeliveryEvent); + $emailDeliveryEvent->setToUser($this); + } + + return $this; + } + + public function removeEmailDeliveryEvent(EmailDeliveryEvent $emailDeliveryEvent): static + { + if ($this->emailDeliveryEvents->removeElement($emailDeliveryEvent)) { + // set the owning side to null (unless already changed) + if ($emailDeliveryEvent->getToUser() === $this) { + $emailDeliveryEvent->setToUser(null); + } + } + + return $this; + } } diff --git a/src/EventSubscriber/EmailCreatedSubscriber.php b/src/EventSubscriber/EmailCreatedSubscriber.php new file mode 100644 index 0000000..6fc0624 --- /dev/null +++ b/src/EventSubscriber/EmailCreatedSubscriber.php @@ -0,0 +1,119 @@ +getMessage(); + if (!($message instanceof EmailMessage)) { + $this->logger->warning('The message is not an instance of Email.', [ + 'message' => $message, + ]); + + return; + } + + $subject = $message->getSubject(); + if (!\is_string($subject)) { + $this->logger->warning('The message does not have a valid subject.', [ + 'message' => $message, + 'subject' => $subject, + ]); + + return; + } + + $textBody = $message->getTextBody(); + if (!\is_string($textBody)) { + $this->logger->warning('The message does not have an valid text body.', [ + 'message' => $message, + 'body' => $textBody, + ]); + + return; + } + + $htmlBody = $message->getHtmlBody(); + if (!\is_string($htmlBody)) { + $this->logger->warning('The message does not have an valid HTML body.', [ + 'message' => $message, + 'body' => $htmlBody, + ]); + + return; + } + + try { + $kind = EmailKind::fromEmailHeader($message->getHeaders()); + } catch (\InvalidArgumentException $exception) { + $this->logger->warning('The message does not have a valid email kind.', [ + 'message' => $message, + 'exception' => $exception, + ]); + + return; + } + + $email = (new EmailEntity()) + ->setSubject($subject) + ->setTextContent($textBody) + ->setHtmlContent($htmlBody) + ->setKind($kind); + $this->entityManager->persist($email); + + /** + * @var list
$recipients + */ + $recipients = [ + ...$message->getTo(), + ...$message->getCc(), + ...$message->getBcc(), + ]; + + foreach ($recipients as $recipient) { + $emailDeliveryEvent = (new EmailDeliveryEvent()) + ->setToAddress($recipient->getAddress()) + ->setEmail($email); + + $user = $this->userRepository->findOneBy([ + 'email' => $recipient->getAddress(), + ]); + if (null !== $user) { + $emailDeliveryEvent->setToUser($user); + } + + $this->entityManager->persist($emailDeliveryEvent); + } + + $this->entityManager->flush(); + } + + public static function getSubscribedEvents(): array + { + return [ + MessageEvent::class => 'onMessageEvent', + ]; + } +} diff --git a/src/Repository/EmailDeliveryEventRepository.php b/src/Repository/EmailDeliveryEventRepository.php new file mode 100644 index 0000000..96c446b --- /dev/null +++ b/src/Repository/EmailDeliveryEventRepository.php @@ -0,0 +1,37 @@ + + */ +class EmailDeliveryEventRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, EmailDeliveryEvent::class); + } + + /** + * Find the email target to the user. + * + * @param User $user The user to find the email target + * + * @return list + */ + public function findBySendTarget(User $user): array + { + return $this->findBy([ + 'toUser' => $user, + ], orderBy: [ + 'createdAt' => 'DESC', + ]); + } +} diff --git a/src/Repository/EmailRepository.php b/src/Repository/EmailRepository.php new file mode 100644 index 0000000..41248d8 --- /dev/null +++ b/src/Repository/EmailRepository.php @@ -0,0 +1,20 @@ + + */ +class EmailRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Email::class); + } +} diff --git a/src/Repository/SolutionEventRepository.php b/src/Repository/SolutionEventRepository.php index 4caab87..350ed28 100644 --- a/src/Repository/SolutionEventRepository.php +++ b/src/Repository/SolutionEventRepository.php @@ -166,28 +166,12 @@ public function listLeaderboard(?Group $group, string $interval): array $qb = $qb->andWhere('u.group IS NULL'); } - $result = $qb->getQuery()->getResult(); - \assert(\is_array($result) && array_is_list($result)); - /** - * @var list $leaderboard + * @var list}> $result */ - $leaderboard = []; - - foreach ($result as $item) { - \assert(\is_array($item)); - \assert(\array_key_exists('user', $item)); - \assert(\array_key_exists('count', $item)); - \assert($item['user'] instanceof User); - \assert(\is_int($item['count'])); - - $leaderboard[] = [ - 'user' => $item['user'], - 'count' => $item['count'], - ]; - } + $result = $qb->getQuery()->getResult(); - return $leaderboard; + return $result; } /** @@ -213,7 +197,7 @@ public function getTotalAttempts(Question $question, ?Group $group): array } /** - * @var SolutionEvent[] $result + * @var list $result */ $result = $qb->getQuery()->getResult(); diff --git a/src/Service/EmailService.php b/src/Service/EmailService.php new file mode 100644 index 0000000..5b52637 --- /dev/null +++ b/src/Service/EmailService.php @@ -0,0 +1,42 @@ +fromAddress = new Address( + address: $this->serverMail, + name: '資料庫練功房' + ); + } + + /** + * Send an email with the given {@link EmailDto}. + * + * @param EmailDto $emailDto the email to send + * + * @throws TransportExceptionInterface + */ + public function send(EmailDto $emailDto): Email + { + $email = $emailDto->toEmail()->from($this->fromAddress); + + $this->mailer->send($email); + + return $email; + } +} diff --git a/src/Twig/Components/Challenge/Comments/CommentForm.php b/src/Twig/Components/Challenge/Comments/CommentForm.php index 10cd494..3e98188 100644 --- a/src/Twig/Components/Challenge/Comments/CommentForm.php +++ b/src/Twig/Components/Challenge/Comments/CommentForm.php @@ -57,8 +57,6 @@ protected function instantiateForm(): FormInterface public function save(EntityManagerInterface $entityManager, ParameterBagInterface $parameterBag): void { $appFeatureComment = $parameterBag->get('app.features.comment'); - \assert(\is_bool($appFeatureComment)); - if (!$appFeatureComment) { throw new BadRequestHttpException('Comment feature is disabled.'); } diff --git a/src/Twig/Components/Challenge/Instruction/Modal.php b/src/Twig/Components/Challenge/Instruction/Modal.php index 04677b2..10d2cb6 100644 --- a/src/Twig/Components/Challenge/Instruction/Modal.php +++ b/src/Twig/Components/Challenge/Instruction/Modal.php @@ -67,8 +67,6 @@ public function instruct( ParameterBagInterface $parameterBag, ): void { $appFeatureHint = $parameterBag->get('app.features.hint'); - \assert(\is_bool($appFeatureHint)); - if (!$appFeatureHint) { throw new BadRequestHttpException('Hint feature is disabled.'); } diff --git a/symfony.lock b/symfony.lock index 5862a09..c6473aa 100644 --- a/symfony.lock +++ b/symfony.lock @@ -38,6 +38,9 @@ "meilisearch/search-bundle": { "version": "dev-main" }, + "notfloran/mjml-bundle": { + "version": "dev-main" + }, "php-http/discovery": { "version": "1.9999999", "recipe": { @@ -79,6 +82,15 @@ "sensiolabs/typescript-bundle": { "version": "dev-main" }, + "symfony/amazon-mailer": { + "version": "7.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "4.4", + "ref": "9648db3ecae5c8a6b1a5f74715d3907124348815" + } + }, "symfony/asset-mapper": { "version": "7.2", "recipe": { diff --git a/templates/admin/email-template/details.twig b/templates/admin/email-template/details.twig new file mode 100644 index 0000000..d0d4cbc --- /dev/null +++ b/templates/admin/email-template/details.twig @@ -0,0 +1,16 @@ +{% extends '@EasyAdmin/page/content.html.twig' %} + +{% block content_title %}郵件範本 – 預覽 {{ name }}{% endblock %} +{% block page_actions %} + 回到範本列表 +{% endblock %} + +{% block main %} +

參數:{{ parameters|json_encode }}(可以在 query string 傳入 parameters={"key": "value"} 參數)

+ + {% if error is not null %} +
發生錯誤:{{ error }}
+ {% else %} + + {% endif %} +{% endblock %} diff --git a/templates/admin/email-template/index.twig b/templates/admin/email-template/index.twig new file mode 100644 index 0000000..3ddd99d --- /dev/null +++ b/templates/admin/email-template/index.twig @@ -0,0 +1,22 @@ +{% extends '@EasyAdmin/page/content.html.twig' %} + +{% block content_title %}郵件範本{% endblock %} + +{% block main %} + + + + + + + + + {% for template in templates %} + + + + + {% endfor %} + +
模板檔案路徑功能
{{ template }}預覽
+{% endblock %} diff --git a/templates/comments/index.html.twig b/templates/comments/index.html.twig index 40e0c57..9ba8a08 100644 --- a/templates/comments/index.html.twig +++ b/templates/comments/index.html.twig @@ -13,7 +13,7 @@ {% for comment in comments %}
  • #{{ comment.id }}・{{ comment.createdAt|date('Y-m-d H:i:s') }}・ {{ comment.commentLikeEvents|length }}

    -
    第 {{ comment.question.id }} 題({{ comment.question.title }})留言了
    +
    第 {{ comment.question.id }} 題({{ comment.question.title }})留言了

    {{ comment.content|striptags }}

  • {% endfor %} diff --git a/templates/components/Challenge/Comments.html.twig b/templates/components/Challenge/Comments.html.twig index 54f5b7f..20b1037 100644 --- a/templates/components/Challenge/Comments.html.twig +++ b/templates/components/Challenge/Comments.html.twig @@ -1,5 +1,5 @@ -
    - {% if appfeatures.comment %} +
    + {% if app_features_comment %}
    diff --git a/templates/components/Challenge/Header.html.twig b/templates/components/Challenge/Header.html.twig index 51ddf6a..0204de5 100644 --- a/templates/components/Challenge/Header.html.twig +++ b/templates/components/Challenge/Header.html.twig @@ -27,7 +27,7 @@ @@ -36,7 +36,7 @@ diff --git a/templates/components/Challenge/SolutionVideoModal.html.twig b/templates/components/Challenge/SolutionVideoModal.html.twig index b54c2ef..c59c097 100644 --- a/templates/components/Challenge/SolutionVideoModal.html.twig +++ b/templates/components/Challenge/SolutionVideoModal.html.twig @@ -4,7 +4,7 @@ 'aria-label': '打開解答影片', 'aria-hidden': 'true', 'data-video-url': path('app_challenge_solution_video', { - id: this.question.id, + question: this.question.id, csrf: csrf_token('challenge-solution'), }), }|merge(stimulus_controller('challenge-solution-video-modal'))) }}> diff --git a/templates/components/Challenge/Ui.html.twig b/templates/components/Challenge/Ui.html.twig index 39ade25..de3d888 100644 --- a/templates/components/Challenge/Ui.html.twig +++ b/templates/components/Challenge/Ui.html.twig @@ -3,7 +3,7 @@ {% if question.solutionVideo %} {% endif %} - {% if appfeatures.hint %} + {% if app_features_hint %} {% endif %} @@ -15,7 +15,7 @@
    - {% if appfeatures.hint %} + {% if app_features_hint %} {% endif %} {% if question.solutionVideo %} @@ -35,7 +35,7 @@
    - {% if appfeatures.comment %} + {% if app_features_comment %}

    留言區

    diff --git a/templates/components/Navbar.html.twig b/templates/components/Navbar.html.twig index 0d9bc31..5a05e0e 100644 --- a/templates/components/Navbar.html.twig +++ b/templates/components/Navbar.html.twig @@ -20,7 +20,7 @@ name: '留言一覽', icon: 'bi bi-chat-left-text-fill', path: path('app_comments'), - disabled: not appfeatures.comment, + disabled: not app_features_comment, }, { pageId: 'complementary', diff --git a/templates/components/Questions/Card.html.twig b/templates/components/Questions/Card.html.twig index b74408a..8e8e992 100644 --- a/templates/components/Questions/Card.html.twig +++ b/templates/components/Questions/Card.html.twig @@ -10,7 +10,7 @@
    - 進行測驗 + 進行測驗 {% set passRate = this.passRate %}
    通過率 {{ passRate.passRate }}%
    diff --git a/templates/email/mjml/_partials/footer.mjml.twig b/templates/email/mjml/_partials/footer.mjml.twig new file mode 100644 index 0000000..e80e816 --- /dev/null +++ b/templates/email/mjml/_partials/footer.mjml.twig @@ -0,0 +1,9 @@ + + + + 你會收到這封郵件,是因為你是資料庫練功房的練習學生。
    + 回報信件問題 +
    +
    +
    diff --git a/templates/email/mjml/_partials/header-situation.mjml.twig b/templates/email/mjml/_partials/header-situation.mjml.twig new file mode 100644 index 0000000..55d47f3 --- /dev/null +++ b/templates/email/mjml/_partials/header-situation.mjml.twig @@ -0,0 +1,10 @@ + + + + + + + diff --git a/templates/email/mjml/remember-to-login.mjml.twig b/templates/email/mjml/remember-to-login.mjml.twig new file mode 100644 index 0000000..4b52781 --- /dev/null +++ b/templates/email/mjml/remember-to-login.mjml.twig @@ -0,0 +1,43 @@ +{% block email_content %} + {% mjml %} + + + + ⚠️ 我注意到這週你沒有登入,記得持續學習和練習對進步非常重要!提醒你一下,如果這週做題數量未達 5 + 題,每少做一題將會扣 4 分,希望你能儘快投入學習,保持進度,這樣才能持續提升自己的 SQL 能力。加油! + + + + + {{ include('email/mjml/_partials/header-situation.mjml.twig') }} + + + + + + + + + + ⚠️ 我注意到這週你沒有登入,記得持續學習和練習對進步非常重要!提醒你一下,如果這週做題數量未達 + 5 + 題,每少做一題將會扣 4 分,希望你能儘快投入學習,保持進度,這樣才能持續提升自己的 SQL 能力。加油! + + + 立即登入 + + + + + + + + + {{ include('email/mjml/_partials/footer.mjml.twig') }} + + + + {% endmjml %} +{% endblock %} diff --git a/templates/email/preview.html.twig b/templates/email/preview.html.twig new file mode 100644 index 0000000..d3d0246 --- /dev/null +++ b/templates/email/preview.html.twig @@ -0,0 +1,77 @@ +{% extends 'app.html.twig' %} + +{% block nav %} + {% endblock %} +{% block title %}信件預覽{% endblock %} + +{% block app %} + {% set textContent = emailDeliveryEvent.email.textContent %} + {% set htmlContent = emailDeliveryEvent.email.htmlContent %} + {% set hasText, hasHtml = textContent|length > 0, htmlContent|length > 0 %} + +
    +
    +
    +

    + + {{ emailDeliveryEvent.email.subject }} +

    + + + +
    + {% if hasHtml %} +
    + +
    + {% endif %} + {% if hasText %} +
    +
    {{ emailDeliveryEvent.email.textContent }}
    +
    + {% endif %} +
    +
    + +
    +
    +{% endblock %} diff --git a/tests/Entity/EmailDtoTest.php b/tests/Entity/EmailDtoTest.php new file mode 100644 index 0000000..30d73ff --- /dev/null +++ b/tests/Entity/EmailDtoTest.php @@ -0,0 +1,57 @@ +setSubject('Test subject') + ->setToAddress(new Address('test@dbplay.pan93.com')) + ->setKind(EmailKind::Test) + ->setText('Test text') + ->setHtml('

    Test text

    '); + + $email = $emailDto->toEmail(); + + self::assertEquals('Test subject', $email->getSubject()); + self::assertEquals('test@dbplay.pan93.com', $email->getTo()[0]->getAddress()); + self::assertEquals('Test text', $email->getTextBody()); + self::assertEquals('

    Test text

    ', $email->getHtmlBody()); + + $extractedKind = EmailKind::fromEmailHeader($email->getHeaders()); + self::assertEquals(EmailKind::Test, $extractedKind); + } + + public function testEmailDtoToUser(): void + { + $user = (new User()) + ->setName('Test name') + ->setEmail('test@dbplay.pan93.com'); + + $emailDto = EmailDto::fromUser($user) + ->setSubject('Test subject') + ->setKind(EmailKind::Test) + ->setText('Test text') + ->setHtml('

    Test text

    '); + + $email = $emailDto->toEmail(); + + self::assertEquals('Test subject', $email->getSubject()); + self::assertEquals('"Test name" ', $email->getTo()[0]->toString()); + self::assertEquals('Test text', $email->getTextBody()); + self::assertEquals('

    Test text

    ', $email->getHtmlBody()); + + $extractedKind = EmailKind::fromEmailHeader($email->getHeaders()); + self::assertEquals(EmailKind::Test, $extractedKind); + } +} diff --git a/tests/Entity/EmailKindTest.php b/tests/Entity/EmailKindTest.php new file mode 100644 index 0000000..a993b45 --- /dev/null +++ b/tests/Entity/EmailKindTest.php @@ -0,0 +1,54 @@ +addToEmailHeader($header); + + self::assertEquals($kind->value, $header->get(EmailKind::EMAIL_HEADER)?->getBodyAsString()); + } + + public function testEmailKindExtractHeader(): void + { + $kind = EmailKind::Transactional; + $header = new Headers(); + $header->addTextHeader(EmailKind::EMAIL_HEADER, $kind->value); + + $extractedKind = EmailKind::fromEmailHeader($header); + + self::assertEquals($kind, $extractedKind); + } + + public function testEmailKindNoHeader(): void + { + $header = new Headers(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The email kind header is missing or is invalid type.'); + + EmailKind::fromEmailHeader($header); + } + + public function testEmailKindInvalidHeader(): void + { + $header = new Headers(); + $header->addTextHeader(EmailKind::EMAIL_HEADER, 'invalid###'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid email kind: invalid###'); + + EmailKind::fromEmailHeader($header); + } +} diff --git a/tests/Entity/LevelTest.php b/tests/Entity/LevelTest.php new file mode 100644 index 0000000..6bf06f0 --- /dev/null +++ b/tests/Entity/LevelTest.php @@ -0,0 +1,38 @@ + + */ + public static function fromPercentDataProvider(): iterable + { + yield [0, Level::Starter]; + yield [4.9, Level::Starter]; + yield [5, Level::Beginner]; + yield [19.9, Level::Beginner]; + yield [20, Level::Intermediate]; + yield [39.9, Level::Intermediate]; + yield [40, Level::Advanced]; + yield [64.9, Level::Advanced]; + yield [65, Level::Expert]; + yield [89.9, Level::Expert]; + yield [90, Level::Master]; + yield [100, Level::Master]; + } +} diff --git a/tests/EventListener/EmailCreatedSubscriberTest.php b/tests/EventListener/EmailCreatedSubscriberTest.php new file mode 100644 index 0000000..45f8bac --- /dev/null +++ b/tests/EventListener/EmailCreatedSubscriberTest.php @@ -0,0 +1,86 @@ +subject('subject') + ->text('body') + ->html('
    bodyfrom('demo-dbplay@example.com') + ->to('test@example.com'); + + $headers = $message->getHeaders(); + $headers = EmailKind::Test->addToEmailHeader($headers); + $message->setHeaders($headers); + + $envelope = Envelope::create($message); + + $userRepository = self::createMock(UserRepository::class); + $userRepository + ->expects(self::once()) + ->method('findOneBy') + ->with(['email' => 'test@example.com']) + ->willReturn(new UserEntity()); + + $invokedCount = self::exactly(2); + /** + * @var Email|null $emailInstance + */ + $emailInstance = null; + $entityManager = self::createMock(EntityManagerInterface::class); + $entityManager + ->expects(self::exactly(2)) + ->method('persist') + ->willReturnCallback(function (mixed ...$parameters) use ($invokedCount, &$emailInstance): void { + switch ($invokedCount->numberOfInvocations()) { + case 1: + $email = $parameters[0]; + \assert($email instanceof EmailEntity); + + self::assertEquals('subject', $email->getSubject()); + self::assertEquals('body', $email->getTextContent()); + self::assertEquals('
    body
    ', $email->getHtmlContent()); + self::assertEquals(EmailKind::Test, $email->getKind()); + + $emailInstance = $email; + break; + case 2: + $event = $parameters[0]; + \assert($event instanceof EmailDeliveryEventEntity); + + self::assertEquals('test@example.com', $event->getToAddress()); + self::assertEquals($emailInstance, $event->getEmail()); + break; + } + }); + + $subscriber = new EmailCreatedSubscriber($logger, $userRepository, $entityManager); + $dispatcher = new EventDispatcher(); + $event = new MessageEvent($message, $envelope, ''); + + $dispatcher->addSubscriber($subscriber); + $dispatcher->dispatch($event); + } +} diff --git a/tests/object-manager.php b/tests/object-manager.php new file mode 100644 index 0000000..c58de6f --- /dev/null +++ b/tests/object-manager.php @@ -0,0 +1,22 @@ +bootEnv(__DIR__.'/../.env'); + +$appEnv = $_SERVER['APP_ENV']; +assert(is_string($appEnv), 'APP_ENV should be specified and must be a string.'); + +$appDebug = $_SERVER['APP_DEBUG'] ?? 'false'; +assert(is_string($appDebug)); + +$kernel = new Kernel($appEnv, (bool) $appDebug); + +$kernel->boot(); + +return $kernel->getContainer()->get('doctrine')->getManager(); diff --git a/translations/messages.zh_TW.yaml b/translations/messages.zh_TW.yaml index 92a3780..eee286c 100644 --- a/translations/messages.zh_TW.yaml +++ b/translations/messages.zh_TW.yaml @@ -59,6 +59,16 @@ System Management: 系統管理 Announcement: 公告 URL: 網址 Published: 發布 +Preview: 預覽 +To User: 收件使用者 +To Address: 收件信箱 +Mails: 郵件 +Subject: 主旨 +EmailDeliveryEvent: 郵件投遞事件 +Kind: 種類 +Text Content: 文字內容 +HTML Content: HTML 內容 +EmailTemplates: 郵件範本 result_presenter.tabs.result: 執行結果 result_presenter.tabs.answer: 正確答案 @@ -173,3 +183,8 @@ challenge: answer-query-failure: 正確答案也是個錯誤的 SQL 查詢:%error% user-query-error: 你的 SQL 查詢執行失敗:%error% user-query-failure: 你的 SQL 查詢不正確:%error% + +email-kind: + transactional: 通知型信件 + marketing: 行銷型信件 + test: 測試用信件 diff --git a/worker.Dockerfile b/worker.Dockerfile new file mode 100644 index 0000000..3aa00dc --- /dev/null +++ b/worker.Dockerfile @@ -0,0 +1,79 @@ +# syntax=docker/dockerfile:1 + +FROM php:8.3-cli AS base + +ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ + +WORKDIR /app +VOLUME /app/var/ + +RUN set -eux; \ + apt-get update \ + && apt-get install -y --no-install-recommends \ + acl=* \ + file=* \ + gettext=* \ + git=* \ + && rm -rf /var/lib/apt/lists/* \ + ; + +RUN set -eux; \ + install-php-extensions \ + @composer \ + apcu \ + curl \ + intl \ + opcache \ + zip \ + redis \ + pdo_pgsql \ + sysvsem \ + ; + +# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser +ENV COMPOSER_ALLOW_SUPERUSER=1 + +ENV PHP_INI_SCAN_DIR=":$PHP_INI_DIR/app.conf.d" + +COPY --link frankenphp/conf.d/10-app.ini $PHP_INI_DIR/app.conf.d/ +COPY --link --chmod=755 frankenphp/docker-entrypoint.sh /usr/local/bin/docker-entrypoint + +ENTRYPOINT ["docker-entrypoint"] + +# Worker +FROM base AS worker +LABEL org.opencontainers.image.source="https://github.com/database-playground/app-sf" + +ENV APP_ENV=prod +ENV APP_DEBUG=0 + +RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" + +COPY --link frankenphp/conf.d/20-app.prod.ini $PHP_INI_DIR/app.conf.d/ + +# prevent the reinstallation of vendors at every changes in the source code +COPY --link composer.* symfony.* package.json* ./ +RUN set -eux; \ + composer install --no-cache --prefer-dist --no-dev --no-autoloader --no-scripts --no-progress; + +# copy sources +COPY --link . ./ +RUN rm -Rf frankenphp/ + +RUN set -eux; \ + mkdir -p var/cache var/log; \ + composer dump-autoload --classmap-authoritative --no-dev; \ + composer dump-env prod; \ + composer run-script --no-dev post-install-cmd; \ + chmod +x bin/console; sync; + +RUN set -eux; \ + chmod +x bin/console; sync; \ + ./bin/console cache:clear; \ + ./bin/console cache:warmup; + +ENV RUN_MIGRATIONS=false + +# Restart the messenger about each 10 minute or when memory limit (300M) is reached +# https://symfony.com/doc/current/messenger.html#deploying-to-production +CMD ["php", "bin/console", "messenger:consume", "--all", "-vv", "--time-limit=600", "--memory-limit=300M"]