diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000000..d26804637a0b7 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,38 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.191.1/containers/ruby/.devcontainer/base.Dockerfile + +# [Choice] Ruby version: 3.4, 3.3, 3.2 +ARG VARIANT="3.4.4" +FROM ghcr.io/rails/devcontainer/images/ruby:${VARIANT} + +RUN sudo apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && sudo apt-get -y install --no-install-recommends \ + mariadb-client libmariadb-dev \ + postgresql-client postgresql-contrib libpq-dev \ + ffmpeg mupdf mupdf-tools libvips-dev poppler-utils \ + libxml2-dev sqlite3 imagemagick + +# Add the Rails main Gemfile and install the gems. This means the gem install can be done +# during build instead of on start. When a fork or branch has different gems, we still have an +# advantage due to caching of the other gems. +RUN mkdir -p /tmp/rails +COPY Gemfile Gemfile.lock RAILS_VERSION rails.gemspec package.json yarn.lock /tmp/rails/ +COPY actioncable/actioncable.gemspec /tmp/rails/actioncable/ +COPY actionmailbox/actionmailbox.gemspec /tmp/rails/actionmailbox/ +COPY actionmailer/actionmailer.gemspec /tmp/rails/actionmailer/ +COPY actionpack/actionpack.gemspec /tmp/rails/actionpack/ +COPY actiontext/actiontext.gemspec /tmp/rails/actiontext/ +COPY actionview/actionview.gemspec /tmp/rails/actionview/ +COPY activejob/activejob.gemspec /tmp/rails/activejob/ +COPY activemodel/activemodel.gemspec /tmp/rails/activemodel/ +COPY activerecord/activerecord.gemspec /tmp/rails/activerecord/ +COPY activestorage/activestorage.gemspec /tmp/rails/activestorage/ +COPY activesupport/activesupport.gemspec /tmp/rails/activesupport/ +COPY railties/railties.gemspec /tmp/rails/railties/ +COPY tools/releaser/releaser.gemspec /tmp/rails/tools/releaser/ +# Docker does not support COPY as users other than root. So we need to chown this dir so we +# can bundle as vscode user and then remove the tmp dir +RUN sudo chown -R vscode:vscode /tmp/rails +USER vscode +RUN cd /tmp/rails \ + && bash -i -c 'bundle install' \ + && rm -rf /tmp/rails diff --git a/.devcontainer/boot.sh b/.devcontainer/boot.sh new file mode 100755 index 0000000000000..ee03e3678a5e6 --- /dev/null +++ b/.devcontainer/boot.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +bundle update --bundler +bundle install + +if [ -n "${NVM_DIR}" ]; then + # shellcheck disable=SC1091 + . "${NVM_DIR}/nvm.sh" && nvm install --lts + yarn install +fi + +cd activerecord || { echo "activerecord directory doesn't exist"; exit; } + +# Create PostgreSQL databases +bundle exec rake db:postgresql:rebuild + +# Create MySQL databases +bundle exec rake db:mysql:rebuild diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml new file mode 100644 index 0000000000000..514c48d4e1179 --- /dev/null +++ b/.devcontainer/compose.yaml @@ -0,0 +1,57 @@ +services: + rails: + build: + context: .. + dockerfile: .devcontainer/Dockerfile + + volumes: + - ../..:/workspaces:cached + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + + depends_on: + - postgres + - mysql + - redis + - memcached + + environment: + MYSQL_CODESPACES: "1" + + # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + + postgres: + image: postgres:latest + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + POSTGRES_USER: postgres + POSTGRES_DB: postgres + POSTGRES_PASSWORD: postgres + + mysql: + image: mysql:latest + restart: unless-stopped + volumes: + - mysql-data:/var/lib/mysql + environment: + MYSQL_ROOT_PASSWORD: root + + redis: + image: valkey/valkey:8 + restart: unless-stopped + volumes: + - redis-data:/data + + memcached: + image: memcached:latest + restart: unless-stopped + command: ["-m", "1024"] + +volumes: + postgres-data: + mysql-data: + redis-data: diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000000..546af855e55c5 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,49 @@ +// For format details, see https://containers.dev/implementors/json_reference/. +{ + "name": "Rails project development", + "dockerComposeFile": "compose.yaml", + "service": "rails", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "latest" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "latest" + }, + "ghcr.io/rails/devcontainer/features/postgres-client:1.1.1": { + "version": "17" + } + }, + + "containerEnv": { + "PGHOST": "postgres", + "PGUSER": "postgres", + "PGPASSWORD": "postgres", + "MYSQL_HOST": "mysql", + "REDIS_URL": "redis://redis/0", + "MEMCACHE_SERVERS": "memcached:11211" + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // This can be used to network with other containers or the host. + // "forwardPorts": [3000, 5432], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": ".devcontainer/boot.sh", + + // Configure tool-specific properties. + "customizations": { + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "Shopify.ruby-lsp" + ] + } + } + + // Uncomment to connect as root instead. More info: https://containers.dev/implementors/json_reference/#remoteUser. + // "remoteUser": "root" +} diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000000..7fede1e72571f --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,86 @@ +# Changes that are cosmetic and do not add anything substantial +# to the stability, functionality, or testability of Rails will +# generally not be accepted. Read more about our rationale behind +# this decision: https://github.com/rails/rails/pull/13771#issuecomment-32746700 + +# normalizes indentation and whitespace across the project +80e66cc4d90bf8c15d1a5f6e3152e90147f00772 +# applies new string literal convention in * +adca8154c6ffce978a5dbc514273cceecbb15f8e +9ed740449884ba5841f756c4a5ccc0bce8f19082 +92e2d16a3c75d549fcd9422a31acd3323b74abaa +78d3f84955bccad0ab161c5f2b4c1133813161a3 +6b3719b7577ab81171bab94a0492ae383dd279fe +8b4e6bf23338e2080af537ea4f295e65a1d11388 +783763bde97bea3d0c200038453008a8cfff1e88 +69ab3eb57e8387b0dd9d672b5e8d9185395baa03 +f8477f13bfe554064bd25a57e5289b4ebaabb504 +b678eb57e93423ac8e2a0cc0b083ce556c6fb130 +b91ff557ef6f621d1b921f358fd5b8a5d9e9090e +c3e7abddfb759eecb30cd59f27103fda4c4827dc +35b3de8021e68649cac963bd82a74cc75d1f60f0 +628e51ff109334223094e30ad1365188bbd7f9c6 +4b6c68dfb810c836f87587a16353317d1a180805 +66a7cfa91045e05f134efc9ac0e226e66161e2e6 +bde6547bb6a8ddf18fb687bf20893d3dc87e0358 +93c9534c9871d4adad4bc33b5edc355672b59c61 +4c208254573c3428d82bd8744860bd86e1c78acb +18a2513729ab90b04b1c86963e7d5b9213270c81 +9617db2078e8a85c8944392c21dd748f932bbd80 +4df2b779ddfcb27761c71e00e2b241bfa06a0950 +a731125f12c5834de7eae3455fad63ea4d348034 +d66e7835bea9505f7003e5038aa19b6ea95ceea1 +e6ab70c439301d533f14b3387ee181d843a86b30 +# modernizes hash syntax in * +1607ee299d15c133b2b63dcfc840eddfba7e525b +477568ee33bee0dc5e57b9df624142296e3951a4 +5c315a8fa6296904f5e0ba8da919fc395548cf98 +d22e522179c1c90e658c3ed0e9b972ec62306209 +fa911a74e15ef34bb435812f7d9cf7324253476f +301ce2a6d11bc7a016f7ede71e3c6fd9fa0693a3 +63fff600accb41b56a3e6ac403d9b1732de3086d +5b6eb1d58b48fada298215b2cccda89f993890c3 +12a70404cd164008879e63cc320356e6afee3adc +60b67d76dc1d98e4269aac7705e9d8323eb42942 +# [Tests only] Enable Minitest/AssertPredicate rule +19f8ab2e7d60dcdfd7664d6bea3a9fae55a3618c +# Standardize nodoc comments +18707ab17fa492eb25ad2e8f9818a320dc20b823 +# Add Style/RedundantFreeze to remove redudant .freeze +aa3dcabd874a3e82e455e85a1c94a7abaac2900a +# Enable Performance/UnfreezeString cop +1b86d90136efb98c7b331a84ca163587307a49af +# Arel: rubocop -a +4c0a3d48804a363c7e9272519665a21f601b5248 +# Add more rubocop rules about whitespaces +fe1f4b2ad56f010a4e9b93d547d63a15953d9dc2 +# Add three new rubocop rules +55f9b8129a50206513264824abb44088230793c2 +# applies remaining conventions across the project +b326e82dc012d81e9698cb1f402502af1788c1e9 +# remove redundant curlies from hash arguments +411ccbdab2608c62aabdb320d52cb02d446bb39c +# Deletes trailing whitespaces (over text files only find * -type f -exec sed 's/[ \t]*$//' -i {} ;) +b95d6e84b00bd926b1118f6a820eca7a870b8c35 +b451de0d6de4df6bc66b274cec73b919f823d5ae +# Replace assert ! with assert_not +a1ac18671a90869ef81d02f2eafe8104e4eea34f +# Use respond_to test helpers +0d50cae996c51630361e8514e1f168b0c48957e1 +# Change refute to assert_not +211adb47e76b358ea15a3f756431c042ab231c23 +# Use assert_predicate and assert_not_predicate +94333a4c31bd10c1f358c538a167e6a4589bae2d +# Use assert_empty and assert_not_empty +82c39e1a0b5114e2d89a80883a41090567a83196 +# Remove extra whitespace +fda1863e1a8c120294c56482631d8254ad6125ff +# Reduce string objects by using \ instead of + or << for concatenating strings +b70fc698e157f2a768ba42efac08c08f4786b01c +# Hash Syntax to 1.9 related changes +eebb9ddf9ba559a510975c486fe59a4edc9da97d +be4a4cd38ff2c2db0f6b69bb72fb3557bd5a6e21 +d20a52930aa80d7f219465d6fc414a68b16ef2a8 +3c580182ff3c16d2247aabc85100bf8c6edb0f82 +5ad7f8ab418f0c760dbb584f7c78d94ce32e9ee3 +a2c843885470dddbad9430963190464a22167921 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000000..fdefb6a67e9d9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,28 @@ +--- +name: Bug report +about: Report an issue with Rails you've discovered +--- + +### Steps to reproduce + + + + +```ruby +# Your reproduction script goes here +``` + +### Expected behavior + + + +### Actual behavior + + + +### System configuration + +**Rails version**: + +**Ruby version**: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000..3ba13e0cec6cb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/issue_template.md b/.github/issue_template.md deleted file mode 100644 index 0eb7cc22a2f8f..0000000000000 --- a/.github/issue_template.md +++ /dev/null @@ -1,14 +0,0 @@ -### Steps to reproduce - - -### Expected behavior - - -### Actual behavior - - -### System configuration -**Rails version**: - -**Ruby version**: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7b0e9215f2ff2..fd53fc45a39de 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,21 +1,45 @@ -### Summary + +Please do not make *Draft* pull requests, as they still send +notifications to everyone watching the Rails repo. -### Other Information +Create a pull request when it is ready for review and feedback +from the Rails team :). - +About this template + +The following template aims to help contributors write a good description for their pull requests. +We'd like you to provide a description of the changes in your pull request (i.e. bugs fixed or features added), the motivation behind the changes, and complete the checklist below before opening a pull request. + +Feel free to discard it if you need to (e.g. when you just fix a typo). --> + +### Motivation / Background + + + +This Pull Request has been created because [REPLACE ME] + +### Detail + +This Pull Request changes [REPLACE ME] + +### Additional information + + + +### Checklist + +Before submitting the PR make sure the following are checked: + +* [ ] This Pull Request is related to one change. Unrelated changes should be opened in separate PRs. +* [ ] Commit message has a detailed description of what changed and why. If this PR fixes a related issue include it in the commit message. Ex: `[Fix #issue-number]` +* [ ] Tests are added or updated if you fix a bug or add a feature. +* [ ] CHANGELOG files are updated for the changed libraries if there is a behavior change or additional feature. Minor bug fixes and documentation changes should not be included. diff --git a/.github/security.md b/.github/security.md index 0d60188f05ff3..dc32fb3a0da2e 100644 --- a/.github/security.md +++ b/.github/security.md @@ -3,10 +3,10 @@ ## Reporting a Vulnerability **Do not open up a GitHub issue if the bug is a security vulnerability in Rails**. -Instead refer to our [security policy](https://rubyonrails.org/security/). +Instead, refer to our [security policy](https://rubyonrails.org/security). ## Supported Versions Security backports are provided for some previous release series. For details of which release series are currently receiving security backports see our -[security policy](https://rubyonrails.org/security/). +[security policy](https://rubyonrails.org/security). diff --git a/.github/stale.yml b/.github/stale.yml index 8740f3c89971d..2b40308582bee 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -10,6 +10,8 @@ exemptLabels: - attached PR - regression - release blocker +# Issues on a milestone will never be considered stale +exemptMilestones: true # Label to use when marking an issue as stale staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable @@ -18,16 +20,11 @@ markComment: > The resources of the Rails team are limited, and so we are asking for your help. - If you can still reproduce this error on the `6-0-stable` branch or on `master`, + If you can still reproduce this error on the `8-0-stable` branch or on `main`, please reply with all of the information you have about it in order to keep the issue open. Thank you for all your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false - -pulls: - markComment: > - This pull request has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. - - Thank you for your contributions. +# Limit to only `issues` or `pulls` +only: issues diff --git a/.github/verba-sequentur.yml b/.github/verba-sequentur.yml new file mode 100644 index 0000000000000..0fc1a9586a9a4 --- /dev/null +++ b/.github/verba-sequentur.yml @@ -0,0 +1,21 @@ +# Documentation: https://github.com/jonathanhefner/verba-sequentur + +"support request": + comment: > + This appears to be a request for technical support. We reserve the + issue tracker for issues only. For technical support questions, + please use the [rubyonrails-talk](https://discuss.rubyonrails.org/c/rubyonrails-talk/7) + forum or [Stack Overflow](https://stackoverflow.com/questions/tagged/ruby-on-rails), + where a wider community can help you. + close: true + +"feature request": + comment: > + This appears to be a feature request. We generally do not take + feature requests, and we reserve the issue tracker for issues only. + We recommend you [try to implement the feature]( + https://guides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-code), + and send us a pull request instead. If you are unsure if the feature + would be accepted, please ask on the [rubyonrails-core]( + https://discuss.rubyonrails.org/c/rubyonrails-core/5) forum. + close: true diff --git a/.github/workflows/devcontainer-shellcheck.yml b/.github/workflows/devcontainer-shellcheck.yml new file mode 100644 index 0000000000000..57eb1c3aa8ca4 --- /dev/null +++ b/.github/workflows/devcontainer-shellcheck.yml @@ -0,0 +1,24 @@ +name: Devcontainer Shellcheck + +on: + pull_request: + paths: + - ".devcontainer/**/*.sh" + push: + paths: + - ".devcontainer/**/*.sh" + +permissions: + contents: read + +jobs: + devcontainer_shellcheck: + name: Devcontainer Shellcheck + runs-on: ubuntu-latest + steps: + - name: Checkout (GitHub) + uses: actions/checkout@v4 + + - name: Lint Devcontainer Scripts + run: | + find .devcontainer/ -name '*.sh' -print0 | xargs -0 shellcheck diff --git a/.github/workflows/devcontainer-smoke-test.yml b/.github/workflows/devcontainer-smoke-test.yml new file mode 100644 index 0000000000000..ae5c1a2e58b7c --- /dev/null +++ b/.github/workflows/devcontainer-smoke-test.yml @@ -0,0 +1,99 @@ +name: Devcontainer smoke test + +on: push + +jobs: + build: + name: Devcontainer smoke test + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + fail-fast: false + + steps: + - name: Checkout (GitHub) + uses: actions/checkout@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.4 + bundler-cache: true + + - name: Generate rails app sqlite3 + run: bundle exec railties/exe/rails new myapp_sqlite --database="sqlite3" --dev --devcontainer + + - name: Test devcontainer sqlite3 + uses: devcontainers/ci@v0.3 + with: + subFolder: myapp_sqlite + imageName: ghcr.io/rails/smoke-test-devcontainer + cacheFrom: ghcr.io/rails/smoke-test-devcontainer + imageTag: sqlite3 + push: ${{ github.repository == 'rails/rails' && 'filter' || 'never' }} + refFilterForPush: refs/heads/main + runCmd: bin/rails g scaffold Post && bin/rails db:migrate && bin/rails test + + - name: Stop all containers + run: docker ps -q | xargs docker stop + + - name: Generate rails app postgresql + run: bundle exec railties/exe/rails new myapp_postgresql --database="postgresql" --dev --devcontainer + + - name: Test devcontainer postgresql + uses: devcontainers/ci@v0.3 + with: + subFolder: myapp_postgresql + imageName: ghcr.io/rails/smoke-test-devcontainer + cacheFrom: ghcr.io/rails/smoke-test-devcontainer + imageTag: postgresql + push: ${{ github.repository == 'rails/rails' && 'filter' || 'never' }} + refFilterForPush: refs/heads/main + runCmd: bin/rails g scaffold Post && bin/rails db:migrate && bin/rails test && bin/rails test:system + + - name: Stop all containers + run: docker ps -q | xargs docker stop + + - name: Generate rails app mysql + run: bundle exec railties/exe/rails new myapp_mysql --database="mysql" --dev --devcontainer + + - name: Test devcontainer mysql + uses: devcontainers/ci@v0.3 + with: + subFolder: myapp_mysql + imageName: ghcr.io/rails/smoke-test-devcontainer + cacheFrom: ghcr.io/rails/smoke-test-devcontainer + imageTag: mysql + push: ${{ github.repository == 'rails/rails' && 'filter' || 'never' }} + refFilterForPush: refs/heads/main + runCmd: bin/rails g scaffold Post && bin/rails db:migrate && bin/rails test + + - name: Stop all containers + run: docker ps -q | xargs docker stop + + - name: Generate rails app trilogy + run: bundle exec railties/exe/rails new myapp_trilogy --database="trilogy" --dev --devcontainer + + - name: Test devcontainer trilogy + uses: devcontainers/ci@v0.3 + with: + subFolder: myapp_trilogy + imageName: ghcr.io/rails/smoke-test-devcontainer + cacheFrom: ghcr.io/rails/smoke-test-devcontainer + imageTag: trilogy + push: ${{ github.repository == 'rails/rails' && 'filter' || 'never' }} + refFilterForPush: refs/heads/main + runCmd: bin/rails g scaffold Post && bin/rails db:migrate && bin/rails test + + - name: Stop all containers + run: docker ps -q | xargs docker stop diff --git a/.github/workflows/rail_inspector.yml b/.github/workflows/rail_inspector.yml new file mode 100644 index 0000000000000..9bc240995ded5 --- /dev/null +++ b/.github/workflows/rail_inspector.yml @@ -0,0 +1,27 @@ +name: Rail Inspector + +on: + pull_request: + paths: + - "tools/rail_inspector/**" + push: + paths: + - "tools/rail_inspector/**" + +permissions: + contents: read + +jobs: + rail_inspector: + name: rail_inspector tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Remove Gemfile.lock + run: rm -f Gemfile.lock + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.4 + bundler-cache: true + - run: cd tools/rail_inspector && bundle exec rake diff --git a/.github/workflows/rails-new-docker.yml b/.github/workflows/rails-new-docker.yml new file mode 100644 index 0000000000000..8b36f0897085c --- /dev/null +++ b/.github/workflows/rails-new-docker.yml @@ -0,0 +1,49 @@ +name: rails-new-docker + +on: [push, pull_request] + +permissions: + contents: read + +env: + APP_NAME: devrails + APP_PATH: dev/devrails + BUNDLE_WITHOUT: db:job:cable:storage + +jobs: + rails-new-docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Remove Gemfile.lock + run: rm -f Gemfile.lock + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.4 + bundler-cache: true + - name: Generate --dev app + run: | + bundle exec railties/exe/rails new $APP_PATH --dev + - name: Build image + run: | + podman build -t $APP_NAME \ + -v $(pwd):$(pwd) \ + -f ./$APP_PATH/Dockerfile \ + ./$APP_PATH + - name: Run container + run: | + podman run --name $APP_NAME \ + -v $(pwd):$(pwd) \ + -e SECRET_KEY_BASE_DUMMY=1 \ + -e DATABASE_URL=sqlite3:storage/production.sqlite3 \ + -p 3000:3000 $APP_NAME & + - name: Test container + run: ruby -r ./.github/workflows/scripts/test-container.rb + + - uses: zzak/action-discord@v8 + continue-on-error: true + if: failure() && github.ref_name == 'main' + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + webhook: ${{ secrets.DISCORD_WEBHOOK }} diff --git a/.github/workflows/rails_releaser_tests.yml b/.github/workflows/rails_releaser_tests.yml new file mode 100644 index 0000000000000..f3c2a0f13bb47 --- /dev/null +++ b/.github/workflows/rails_releaser_tests.yml @@ -0,0 +1,29 @@ +name: Rails releaser tests + +on: + pull_request: + paths: + - "tools/releaser/**" + push: + paths: + - "tools/releaser/**" + +permissions: + contents: read + +jobs: + releaser_tests: + name: releaser tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby + - name: Bundle install + run: bundle install + working-directory: tools/releaser + - run: bundle exec rake + working-directory: tools/releaser diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000000..e5c84b75eda69 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +name: Release + +on: + release: + types: [published] + +jobs: + release: + permissions: + contents: write + id-token: write + + environment: release + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby + - uses: actions/setup-node@v4 + with: + node-version: lts/* + registry-url: 'https://registry.npmjs.org' + - name: Configure trusted publishing credentials + uses: rubygems/configure-rubygems-credentials@v1.0.0 + - name: Bundle install + run: bundle install + working-directory: tools/releaser + - name: Run release rake task + run: bundle exec rake push + shell: bash + working-directory: tools/releaser + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Wait for release to propagate + run: gem exec rubygems-await pkg/*.gem + shell: bash diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml deleted file mode 100644 index 3ac81587a60e0..0000000000000 --- a/.github/workflows/rubocop.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: RuboCop - -on: [push, pull_request] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Ruby 2.7 - uses: ruby/setup-ruby@v1 - with: - ruby-version: 2.7 - - name: Cache gems - uses: actions/cache@v1 - with: - path: vendor/bundle - key: ${{ runner.os }}-rubocop-${{ hashFiles('**/Gemfile.lock') }} - restore-keys: | - ${{ runner.os }}-rubocop- - - name: Install gems - run: | - bundle config path vendor/bundle - bundle config set without 'default doc job cable storage ujs test db' - bundle install --jobs 4 --retry 3 - - name: Run RuboCop - run: bundle exec rubocop --parallel diff --git a/.github/workflows/scripts/test-container.rb b/.github/workflows/scripts/test-container.rb new file mode 100644 index 0000000000000..1ebecd2aa3c21 --- /dev/null +++ b/.github/workflows/scripts/test-container.rb @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Based on Sam Ruby's system test from dockerfile-rails: +# https://github.com/rubys/dockerfile-rails/pull/21 + +require "net/http" +require "socket" + +LOCALHOST = Socket.gethostname +PORT = 3000 + +60.times do |i| + sleep 0.5 + begin + response = Net::HTTP.get_response(LOCALHOST, "/up", PORT) + + if %w(404 500).include? response.code + status = response.code.to_i + end + + puts response.body + exit status || 0 + rescue SystemCallError, IOError + puts "#{i}/60 Connection to #{LOCALHOST}:#{PORT} refused or timed out, retrying..." + end +end + +exit 999 diff --git a/.gitignore b/.gitignore index 71acd2c6387cf..91ec62d2f9a7a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ # Check out https://help.github.com/articles/ignoring-files for how to set that up. .Gemfile -.byebug_history .ruby-version /*/doc/ /*/test/tmp/ @@ -10,7 +9,10 @@ /dist/ /doc/ /guides/output/ -debug.log +/preview/ +preview.tar.gz +Brewfile.lock.json +debug.log* node_modules/ package-lock.json pkg/ diff --git a/.mdlrc b/.mdlrc new file mode 100644 index 0000000000000..4e92d0f58cb91 --- /dev/null +++ b/.mdlrc @@ -0,0 +1 @@ +style "#{File.dirname(__FILE__)}/.mdlrc.rb" \ No newline at end of file diff --git a/.mdlrc.rb b/.mdlrc.rb new file mode 100644 index 0000000000000..78d01e5ccde72 --- /dev/null +++ b/.mdlrc.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +all + +exclude_rule "MD003" +exclude_rule "MD004" +exclude_rule "MD005" +exclude_rule "MD006" +exclude_rule "MD007" +exclude_rule "MD012" +exclude_rule "MD014" +exclude_rule "MD024" +exclude_rule "MD026" +exclude_rule "MD033" +exclude_rule "MD034" +exclude_rule "MD036" +exclude_rule "MD040" +exclude_rule "MD041" + +rule "MD013", line_length: 2000, ignore_code_blocks: true +# rule "MD024", allow_different_nesting: true # This did not work as intended, see action_cable_overview.md +rule "MD029", style: :ordered +# rule "MD046", style: :consistent # default (:fenced) diff --git a/.rubocop.yml b/.rubocop.yml index 2b64b50e39ceb..a1bae07239aa7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,21 +1,33 @@ -require: +plugins: + - rubocop-minitest + - rubocop-packaging - rubocop-performance - rubocop-rails + - rubocop-md AllCops: - TargetRubyVersion: 2.5 # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop # to ignore them, so only the ones explicitly set in this file are enabled. DisabledByDefault: true + SuggestExtensions: false Exclude: - '**/tmp/**/*' - '**/templates/**/*' - '**/vendor/**/*' - - 'actionpack/lib/action_dispatch/journey/parser.rb' - - 'railties/test/fixtures/tmp/**/*' - 'actionmailbox/test/dummy/**/*' + - 'activestorage/test/dummy/**/*' - 'actiontext/test/dummy/**/*' + - 'tools/rail_inspector/test/fixtures/*' + - guides/source/debugging_rails_applications.md + - guides/source/active_support_instrumentation.md - '**/node_modules/**/*' + - '**/CHANGELOG.md' + - '**/2_*_release_notes.md' + - '**/3_*_release_notes.md' + - '**/4_*_release_notes.md' + - '**/5_*_release_notes.md' + - '**/6_*_release_notes.md' + Performance: Exclude: @@ -31,21 +43,29 @@ Rails/RefuteMethods: Include: - '**/test/**/*' +Rails/IndexBy: + Enabled: true + +Rails/IndexWith: + Enabled: true + # Prefer &&/|| over and/or. Style/AndOr: Enabled: true -# Align `when` with `case`. -Layout/CaseIndentation: +Layout/ClosingHeredocIndentation: Enabled: true -Layout/ClosingHeredocIndentation: +Layout/ClosingParenthesisIndentation: Enabled: true # Align comments with method definitions. Layout/CommentIndentation: Enabled: true +Layout/DefEndAlignment: + Enabled: true + Layout/ElseAlignment: Enabled: true @@ -56,6 +76,9 @@ Layout/EndAlignment: EnforcedStyleAlignWith: variable AutoCorrect: true +Layout/EndOfLine: + Enabled: true + Layout/EmptyLineAfterMagicComment: Enabled: true @@ -81,15 +104,15 @@ Layout/EmptyLinesAroundModuleBody: # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. Style/HashSyntax: Enabled: true - -Layout/FirstArgumentIndentation: - Enabled: true + EnforcedShorthandSyntax: either # Method definitions after `private` or `protected` isolated calls need one # extra level of indentation. Layout/IndentationConsistency: Enabled: true EnforcedStyle: indented_internal_methods + Exclude: + - '**/*.md' # Two spaces, no tabs (for indentation). Layout/IndentationWidth: @@ -113,6 +136,9 @@ Layout/SpaceAroundEqualsInParameterDefault: Layout/SpaceAroundKeyword: Enabled: true +Layout/SpaceAroundOperators: + Enabled: true + Layout/SpaceBeforeComma: Enabled: true @@ -129,6 +155,9 @@ Style/DefWithParentheses: Style/MethodDefParentheses: Enabled: true +Style/ExplicitBlockArgument: + Enabled: true + Style/FrozenStringLiteralComment: Enabled: true EnforcedStyle: always @@ -141,6 +170,10 @@ Style/FrozenStringLiteralComment: - 'activestorage/db/update_migrate/**/*.rb' - 'actionmailbox/db/migrate/**/*.rb' - 'actiontext/db/migrate/**/*.rb' + - '**/*.md' + +Style/MapToHash: + Enabled: true Style/RedundantFreeze: Enabled: true @@ -167,7 +200,7 @@ Style/StringLiterals: EnforcedStyle: double_quotes # Detect hard tabs, no hard tabs. -Layout/Tab: +Layout/IndentationStyle: Enabled: true # Empty lines should not have any spaces. @@ -182,25 +215,57 @@ Layout/TrailingWhitespace: Style/RedundantPercentQ: Enabled: true +Lint/NestedMethodDefinition: + Enabled: true + Lint/AmbiguousOperator: Enabled: true Lint/AmbiguousRegexpLiteral: Enabled: true +Lint/Debugger: + Enabled: true + DebuggerRequires: + - debug + +Lint/DuplicateRequire: + Enabled: true + +Lint/DuplicateMagicComment: + Enabled: true + +Lint/DuplicateMethods: + Enabled: true + Lint/ErbNewArguments: Enabled: true +Lint/EnsureReturn: + Enabled: true + +Lint/MissingCopEnableDirective: + Enabled: true + # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. Lint/RequireParentheses: Enabled: true -Lint/ShadowingOuterLocalVariable: +Lint/RedundantCopDisableDirective: + Enabled: true + +Lint/RedundantCopEnableDirective: + Enabled: true + +Lint/RedundantRequireStatement: Enabled: true Lint/RedundantStringCoercion: Enabled: true +Lint/RedundantSafeNavigation: + Enabled: true + Lint/UriEscapeUnescape: Enabled: true @@ -210,6 +275,19 @@ Lint/UselessAssignment: Lint/DeprecatedClassMethods: Enabled: true +Lint/InterpolationCheck: + Enabled: true + Exclude: + - '**/test/**/*' + +Lint/SafeNavigationChain: + Enabled: true + +Style/EvalWithLocation: + Enabled: true + Exclude: + - '**/test/**/*' + Style/ParenthesesAroundCondition: Enabled: true @@ -226,6 +304,9 @@ Style/RedundantReturn: Enabled: true AllowMultipleReturnValues: true +Style/RedundantRegexpEscape: + Enabled: true + Style/Semicolon: Enabled: true AllowAsExpressionSeparator: true @@ -237,9 +318,34 @@ Style/ColonMethodCall: Style/TrivialAccessors: Enabled: true +# Prefer a = b || c over a = b ? b : c +Style/RedundantCondition: + Enabled: true + +Style/RedundantDoubleSplatHashBraces: + Enabled: true + +Style/OpenStructUse: + Enabled: true + +Style/ArrayIntersect: + Enabled: true + +Style/KeywordArgumentsMerging: + Enabled: true + +Performance/BindCall: + Enabled: true + Performance/FlatMap: Enabled: true +Performance/MapCompact: + Enabled: true + +Performance/SelectMap: + Enabled: true + Performance/RedundantMerge: Enabled: true @@ -255,5 +361,50 @@ Performance/RegexpMatch: Performance/ReverseEach: Enabled: true -Performance/UnfreezeString: +Performance/StringReplacement: + Enabled: true + +Performance/DeletePrefix: + Enabled: true + +Performance/DeleteSuffix: Enabled: true + +Performance/InefficientHashSearch: + Enabled: true + +Performance/ConstantRegexp: + Enabled: true + +Performance/RedundantStringChars: + Enabled: true + +Performance/StringInclude: + Enabled: true + +Minitest/AssertNil: + Enabled: true + +Minitest/AssertRaisesWithRegexpArgument: + Enabled: true + +Minitest/AssertWithExpectedArgument: + Enabled: true + +Minitest/LiteralAsActualArgument: + Enabled: true + +Minitest/NonExecutableTestMethod: + Enabled: true + +Minitest/SkipEnsure: + Enabled: true + +Minitest/UnreachableAssertion: + Enabled: true + +Markdown: + # Whether to run RuboCop against non-valid snippets + WarnInvalid: true + # Whether to lint codeblocks without code attributes + Autodetect: false diff --git a/Brewfile b/Brewfile index 076d921136003..f767e801d1f1b 100644 --- a/Brewfile +++ b/Brewfile @@ -1,16 +1,14 @@ # frozen_string_literal: true -tap "homebrew/core" -tap "homebrew/bundle" -tap "homebrew/services" -tap "homebrew/cask" brew "ffmpeg" brew "memcached" brew "mysql" -brew "postgresql" +brew "postgresql@16" +brew "libpq" brew "redis" brew "yarn" cask "xquartz" brew "mupdf" brew "poppler" brew "imagemagick" +brew "vips" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 2288e6a7d0c98..d3c1555f706fa 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -4,9 +4,8 @@ The Rails team is committed to fostering a welcoming community. **Our Code of Conduct can be found here**: -https://rubyonrails.org/conduct/ +https://rubyonrails.org/conduct For a history of updates, see the page history here: -https://github.com/rails/homepage/commits/master/conduct.html - +https://github.com/rails/website/commits/main/_pages/conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7e34cecb2e902..51e1236001b8a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,18 +1,29 @@ +[![Build Status](https://badge.buildkite.com/ab1152b6a1f6a61d3ea4ec5b3eece8d4c2b830998459c75352.svg?branch=main)](https://buildkite.com/rails/rails) +[![Code Triage Badge](https://www.codetriage.com/rails/rails/badges/users.svg)](https://www.codetriage.com/rails/rails) +[![Version](https://img.shields.io/gem/v/rails)](https://rubygems.org/gems/rails) +[![License](https://img.shields.io/github/license/rails/rails)](https://github.com/rails/rails) + ## How to contribute to Ruby on Rails #### **Did you find a bug?** * **Do not open up a GitHub issue if the bug is a security vulnerability - in Rails**, and instead to refer to our [security policy](https://rubyonrails.org/security/). + in Rails**, and instead to refer to our [security policy](https://rubyonrails.org/security). * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/rails/rails/issues). * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/rails/rails/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. * If possible, use the relevant bug report templates to create the issue. Simply copy the content of the appropriate template into a .rb file, make the necessary changes to demonstrate the issue, and **paste the content into the issue description**: - * [**Active Record** (models, database) issues](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb) - * [**Action Pack** (controllers, routing) issues](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb) - * [**Generic template** for other issues](https://github.com/rails/rails/blob/master/guides/bug_report_templates/generic_master.rb) + * [**Active Record** (models, encryption, database) issues](https://github.com/rails/rails/blob/main/guides/bug_report_templates/active_record.rb) + * [**Active Record Migrations** issues](https://github.com/rails/rails/blob/main/guides/bug_report_templates/active_record_migrations.rb) + * [**Action View** (views, helpers) issues](https://github.com/rails/rails/blob/main/guides/bug_report_templates/action_view.rb) + * [**Active Job** issues](https://github.com/rails/rails/blob/main/guides/bug_report_templates/active_job.rb) + * [**Active Storage** issues](https://github.com/rails/rails/blob/main/guides/bug_report_templates/active_storage.rb) + * [**Action Mailer** issues](https://github.com/rails/rails/blob/main/guides/bug_report_templates/action_mailer.rb) + * [**Action Mailbox** issues](https://github.com/rails/rails/blob/main/guides/bug_report_templates/action_mailbox.rb) + * [**Action Pack** (controllers, routing) issues](https://github.com/rails/rails/blob/main/guides/bug_report_templates/action_controller.rb) + * [**Generic template** for other issues](https://github.com/rails/rails/blob/main/guides/bug_report_templates/generic.rb) * For more detailed information on submitting a bug report and creating an issue, visit our [reporting guidelines](https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#reporting-an-issue). @@ -34,6 +45,8 @@ Changes that are cosmetic in nature and do not add anything substantial to the s * Do not open an issue on GitHub until you have collected positive feedback about the change. GitHub issues are primarily intended for bug reports and fixes. +* We generally reject changes to Active Support core extensions. Those change should be proposed in the [Ruby issue tracker instead](https://bugs.ruby-lang.org/issues), as we don't want to conflict with future versions of Ruby. + #### **Do you have questions about the source code?** * Ask any question about how to use Ruby on Rails in the [rubyonrails-talk mailing list](https://discuss.rubyonrails.org/c/rubyonrails-talk). @@ -42,7 +55,7 @@ Changes that are cosmetic in nature and do not add anything substantial to the s * Please read [Contributing to the Rails Documentation](https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation). -Ruby on Rails is a volunteer effort. We encourage you to pitch in and [join the team](https://contributors.rubyonrails.org)! +Ruby on Rails is a volunteer effort. We encourage you to pitch in and join [the team](https://contributors.rubyonrails.org)! Thanks! :heart: :heart: :heart: diff --git a/Gemfile b/Gemfile index 75cbddfd77630..6512ce889c746 100644 --- a/Gemfile +++ b/Gemfile @@ -1,21 +1,33 @@ # frozen_string_literal: true source "https://rubygems.org" - -git_source(:github) { |repo| "https://github.com/#{repo}.git" } - gemspec +gem "minitest" + # We need a newish Rake since Active Job sets its test tasks' descriptions. -gem "rake", ">= 11.1" +gem "rake", ">= 13" -gem "capybara", ">= 3.26" -gem "selenium-webdriver", ">= 3.141.592" +gem "releaser", path: "tools/releaser" + +gem "sprockets-rails", ">= 2.0.0", require: false +gem "propshaft", ">= 0.1.7", "!= 1.0.1" +gem "capybara", ">= 3.39" +gem "selenium-webdriver", ">= 4.20.0" gem "rack-cache", "~> 1.2" -gem "sass-rails" -gem "turbolinks", "~> 5" -gem "webpacker", "~> 5.0", require: ENV["SKIP_REQUIRE_WEBPACKER"] != "true" +gem "stimulus-rails" +gem "turbo-rails" +gem "jsbundling-rails" +gem "cssbundling-rails" +gem "importmap-rails", ">= 1.2.3" +gem "tailwindcss-rails" +gem "dartsass-rails" +gem "solid_cache" +gem "solid_queue" +gem "solid_cable" +gem "kamal", ">= 2.1.0", require: false +gem "thruster", require: false # require: false so bcrypt is loaded only when has_secure_password is used. # This is to avoid Active Model (and by extension the entire framework) # being dependent on a binary library. @@ -23,70 +35,94 @@ gem "bcrypt", "~> 3.1.11", require: false # This needs to be with require false to avoid it being automatically loaded by # sprockets. -gem "uglifier", ">= 1.3.0", require: false +gem "terser", ">= 1.1.4", require: false # Explicitly avoid 1.x that doesn't support Ruby 2.4+ -gem "json", ">= 2.0.0" +gem "json", ">= 2.0.0", "!=2.7.0" + +# Workaround until all supported Ruby versions ship with uri version 0.13.1 or higher. +gem "uri", ">= 0.13.1", require: false + +gem "prism" group :rubocop do - gem "rubocop", ">= 0.47", require: false + # Rubocop has to be locked in the Gemfile because CI ignores Gemfile.lock + # We don't want rubocop to start failing whenever rubocop makes a new release. + gem "rubocop", "< 1.73", require: false + gem "rubocop-minitest", require: false + gem "rubocop-packaging", require: false gem "rubocop-performance", require: false gem "rubocop-rails", require: false + gem "rubocop-md", require: false + + # This gem is used in Railties tests so it must be a development dependency. + gem "rubocop-rails-omakase", require: false +end + +group :mdl do + gem "mdl", "!= 0.13.0", require: false end group :doc do - gem "sdoc", "~> 1.1" - gem "redcarpet", "~> 3.2.3", platforms: :ruby - gem "w3c_validators" - gem "kindlerb", "~> 1.2.0" + gem "sdoc", git: "https://github.com/rails/sdoc.git", branch: "main" + gem "rdoc", "< 6.10" + gem "redcarpet", "~> 3.6.1", platforms: :ruby + gem "w3c_validators", "~> 1.3.6" + gem "rouge" + gem "rubyzip", "~> 2.0" end # Active Support -gem "dalli" -gem "listen", "~> 3.2", require: false +gem "dalli", ">= 3.0.1" +gem "listen", "~> 3.3", require: false gem "libxml-ruby", platforms: :ruby gem "connection_pool", require: false gem "rexml", require: false +gem "msgpack", ">= 1.7.0", require: false -# for railties app_generator_test +# for railties gem "bootsnap", ">= 1.4.4", require: false +gem "webrick", require: false +gem "jbuilder", require: false +gem "web-console", require: false + +# Action Pack and railties +rack_version = ENV.fetch("RACK", "~> 3.0") +if rack_version != "head" + gem "rack", rack_version +else + gem "rack", git: "https://github.com/rack/rack.git", branch: "main" +end + +gem "useragent", require: false # Active Job group :job do gem "resque", require: false gem "resque-scheduler", require: false - gem "sidekiq", require: false + gem "sidekiq", "!= 8.0.3", require: false gem "sucker_punch", require: false - gem "delayed_job", require: false - gem "queue_classic", github: "QueueClassic/queue_classic", require: false, platforms: :ruby + gem "queue_classic", ">= 4.0.0", require: false, platforms: :ruby gem "sneakers", require: false - gem "que", require: false gem "backburner", require: false - gem "delayed_job_active_record", require: false - gem "sequel", require: false end # Action Cable group :cable do - gem "puma", require: false + gem "puma", ">= 5.0.3", require: false - gem "hiredis", require: false - gem "redis", "~> 4.0", require: false + gem "redis", ">= 4.0.1", require: false gem "redis-namespace" - gem "websocket-client-simple", github: "matthewd/websocket-client-simple", branch: "close-race", require: false - - gem "blade", require: false, platforms: [:ruby] - gem "blade-sauce_labs_plugin", require: false, platforms: [:ruby] - gem "sprockets-export", require: false + gem "websocket-client-simple", require: false end # Active Storage group :storage do gem "aws-sdk-s3", require: false gem "google-cloud-storage", "~> 1.11", require: false - gem "azure-storage-blob", require: false + gem "azure-storage-blob", "~> 2.0", require: false gem "image_processing", "~> 1.2" end @@ -95,72 +131,39 @@ end gem "aws-sdk-sns", require: false gem "webmock" -group :ujs do - gem "qunit-selenium" - gem "webdrivers" -end - # Add your own local bundler stuff. local_gemfile = File.expand_path(".Gemfile", __dir__) instance_eval File.read local_gemfile if File.exist? local_gemfile group :test do - gem "minitest-bisect" + gem "minitest-bisect", require: false + gem "minitest-ci", require: false gem "minitest-retry" - gem "minitest-reporters" platforms :mri do gem "stackprof" - gem "byebug" + gem "debug", ">= 1.1.0", require: false end - gem "benchmark-ips" + # Needed for Railties tests because it is included in generated apps. + gem "brakeman" + gem "bundler-audit" end -platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do - gem "nokogiri", ">= 1.8.1" - - # Needed for compiling the ActionDispatch::Journey parser. - gem "racc", ">=1.4.6", require: false +platforms :ruby, :windows do + gem "nokogiri", ">= 1.8.1", "!= 1.11.0" # Active Record. - gem "sqlite3", "~> 1.4" + gem "sqlite3", ">= 2.1" group :db do - gem "pg", ">= 0.18.0" + gem "pg", "~> 1.3" gem "mysql2", "~> 0.5" + gem "trilogy", ">= 2.7.0" end end -platforms :jruby do - if ENV["AR_JDBC"] - gem "activerecord-jdbcsqlite3-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "master" - group :db do - gem "activerecord-jdbcmysql-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "master" - gem "activerecord-jdbcpostgresql-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "master" - end - else - gem "activerecord-jdbcsqlite3-adapter", ">= 1.3.0" - group :db do - gem "activerecord-jdbcmysql-adapter", ">= 1.3.0" - gem "activerecord-jdbcpostgresql-adapter", ">= 1.3.0" - end - end -end - -platforms :rbx do - # The rubysl-yaml gem doesn't ship with Psych by default as it needs - # libyaml that isn't always available. - gem "psych", "~> 3.0" -end - -# Gems that are necessary for Active Record tests with Oracle. -if ENV["ORACLE_ENHANCED"] - platforms :ruby do - gem "ruby-oci8", "~> 2.2" - end - gem "activerecord-oracle_enhanced-adapter", github: "rsim/oracle-enhanced", branch: "master" -end +gem "tzinfo-data", platforms: [:windows, :jruby] +gem "wdm", ">= 0.1.0", platforms: [:windows] -gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] -gem "wdm", ">= 0.1.0", platforms: [:mingw, :mswin, :x64_mingw, :mswin64] +gem "launchy" diff --git a/Gemfile.lock b/Gemfile.lock index 6a8498c97460d..eed557d3dda11 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,470 +1,610 @@ GIT - remote: https://github.com/QueueClassic/queue_classic.git - revision: 655143b7952fa011346a00f94d628407aa4e0056 + remote: https://github.com/rails/sdoc.git + revision: cd75e36ce2d1acb66734c1390ffe33aa05479380 + branch: main specs: - queue_classic (4.0.0.pre.alpha1) - pg (>= 0.17, < 2.0) - -GIT - remote: https://github.com/matthewd/websocket-client-simple.git - revision: e161305f1a466b9398d86df3b1731b03362da91b - branch: close-race - specs: - websocket-client-simple (0.3.0) - event_emitter - websocket + sdoc (3.0.0.alpha) + nokogiri + rdoc (>= 5.0) + rouge PATH remote: . specs: - actioncable (6.1.0.alpha) - actionpack (= 6.1.0.alpha) - activesupport (= 6.1.0.alpha) + actioncable (8.1.0.alpha) + actionpack (= 8.1.0.alpha) + activesupport (= 8.1.0.alpha) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.0.alpha) - actionpack (= 6.1.0.alpha) - activejob (= 6.1.0.alpha) - activerecord (= 6.1.0.alpha) - activestorage (= 6.1.0.alpha) - activesupport (= 6.1.0.alpha) - mail (>= 2.7.1) - actionmailer (6.1.0.alpha) - actionpack (= 6.1.0.alpha) - actionview (= 6.1.0.alpha) - activejob (= 6.1.0.alpha) - activesupport (= 6.1.0.alpha) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.1.0.alpha) - actionview (= 6.1.0.alpha) - activesupport (= 6.1.0.alpha) - rack (~> 2.0, >= 2.0.8) + zeitwerk (~> 2.6) + actionmailbox (8.1.0.alpha) + actionpack (= 8.1.0.alpha) + activejob (= 8.1.0.alpha) + activerecord (= 8.1.0.alpha) + activestorage (= 8.1.0.alpha) + activesupport (= 8.1.0.alpha) + mail (>= 2.8.0) + actionmailer (8.1.0.alpha) + actionpack (= 8.1.0.alpha) + actionview (= 8.1.0.alpha) + activejob (= 8.1.0.alpha) + activesupport (= 8.1.0.alpha) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.0.alpha) + actionview (= 8.1.0.alpha) + activesupport (= 8.1.0.alpha) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.0.alpha) - actionpack (= 6.1.0.alpha) - activerecord (= 6.1.0.alpha) - activestorage (= 6.1.0.alpha) - activesupport (= 6.1.0.alpha) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.1.0.alpha) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.0.alpha) + activerecord (= 8.1.0.alpha) + activestorage (= 8.1.0.alpha) + activesupport (= 8.1.0.alpha) + globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (6.1.0.alpha) - activesupport (= 6.1.0.alpha) + actionview (8.1.0.alpha) + activesupport (= 8.1.0.alpha) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.0.alpha) - activesupport (= 6.1.0.alpha) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.0.alpha) + activesupport (= 8.1.0.alpha) globalid (>= 0.3.6) - activemodel (6.1.0.alpha) - activesupport (= 6.1.0.alpha) - activerecord (6.1.0.alpha) - activemodel (= 6.1.0.alpha) - activesupport (= 6.1.0.alpha) - activestorage (6.1.0.alpha) - actionpack (= 6.1.0.alpha) - activejob (= 6.1.0.alpha) - activerecord (= 6.1.0.alpha) - activesupport (= 6.1.0.alpha) - marcel (~> 0.3.1) - activesupport (6.1.0.alpha) - concurrent-ruby (~> 1.0, >= 1.0.2) + activemodel (8.1.0.alpha) + activesupport (= 8.1.0.alpha) + activerecord (8.1.0.alpha) + activemodel (= 8.1.0.alpha) + activesupport (= 8.1.0.alpha) + timeout (>= 0.4.0) + activestorage (8.1.0.alpha) + actionpack (= 8.1.0.alpha) + activejob (= 8.1.0.alpha) + activerecord (= 8.1.0.alpha) + activesupport (= 8.1.0.alpha) + marcel (~> 1.0) + activesupport (8.1.0.alpha) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) - minitest (~> 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.2, >= 2.2.2) - rails (6.1.0.alpha) - actioncable (= 6.1.0.alpha) - actionmailbox (= 6.1.0.alpha) - actionmailer (= 6.1.0.alpha) - actionpack (= 6.1.0.alpha) - actiontext (= 6.1.0.alpha) - actionview (= 6.1.0.alpha) - activejob (= 6.1.0.alpha) - activemodel (= 6.1.0.alpha) - activerecord (= 6.1.0.alpha) - activestorage (= 6.1.0.alpha) - activesupport (= 6.1.0.alpha) - bundler (>= 1.3.0) - railties (= 6.1.0.alpha) - sprockets-rails (>= 2.0.0) - railties (6.1.0.alpha) - actionpack (= 6.1.0.alpha) - activesupport (= 6.1.0.alpha) - method_source - rake (>= 0.8.7) - thor (~> 1.0) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + rails (8.1.0.alpha) + actioncable (= 8.1.0.alpha) + actionmailbox (= 8.1.0.alpha) + actionmailer (= 8.1.0.alpha) + actionpack (= 8.1.0.alpha) + actiontext (= 8.1.0.alpha) + actionview (= 8.1.0.alpha) + activejob (= 8.1.0.alpha) + activemodel (= 8.1.0.alpha) + activerecord (= 8.1.0.alpha) + activestorage (= 8.1.0.alpha) + activesupport (= 8.1.0.alpha) + bundler (>= 1.15.0) + railties (= 8.1.0.alpha) + railties (8.1.0.alpha) + actionpack (= 8.1.0.alpha) + activesupport (= 8.1.0.alpha) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + +PATH + remote: tools/releaser + specs: + releaser (1.0.0) + minitest + rake (~> 13.0) GEM remote: https://rubygems.org/ specs: - activerecord-jdbc-adapter (60.1-java) - activerecord (~> 6.0.0) - activerecord-jdbcmysql-adapter (60.1-java) - activerecord-jdbc-adapter (= 60.1) - jdbc-mysql (~> 5.1.36, < 9) - activerecord-jdbcpostgresql-adapter (60.1-java) - activerecord-jdbc-adapter (= 60.1) - jdbc-postgres (>= 9.4, < 43) - activerecord-jdbcsqlite3-adapter (60.1-java) - activerecord-jdbc-adapter (= 60.1) - jdbc-sqlite3 (~> 3.8, < 3.30) - addressable (2.7.0) - public_suffix (>= 2.0.2, < 5.0) - amq-protocol (2.3.0) - ansi (1.5.0) - ast (2.4.0) - aws-eventstream (1.0.3) - aws-partitions (1.260.0) - aws-sdk-core (3.86.0) - aws-eventstream (~> 1.0, >= 1.0.2) - aws-partitions (~> 1, >= 1.239.0) - aws-sigv4 (~> 1.1) - jmespath (~> 1.0) - aws-sdk-kms (1.27.0) - aws-sdk-core (~> 3, >= 3.71.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.60.1) - aws-sdk-core (~> 3, >= 3.83.0) + action_text-trix (2.1.15) + railties + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + amq-protocol (2.3.2) + ast (2.4.2) + aws-eventstream (1.3.0) + aws-partitions (1.1037.0) + aws-sdk-core (3.215.1) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.96.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.177.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.1) - aws-sdk-sns (1.21.0) - aws-sdk-core (~> 3, >= 3.71.0) - aws-sigv4 (~> 1.1) - aws-sigv4 (1.1.0) - aws-eventstream (~> 1.0, >= 1.0.2) - azure-storage-blob (2.0.0) + aws-sigv4 (~> 1.5) + aws-sdk-sns (1.92.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.11.0) + aws-eventstream (~> 1, >= 1.0.2) + azure-storage-blob (2.0.3) azure-storage-common (~> 2.0) - nokogiri (~> 1.10.4) - azure-storage-common (2.0.1) + nokogiri (~> 1, >= 1.10.8) + azure-storage-common (2.0.4) faraday (~> 1.0) - faraday_middleware (~> 1.0.0.rc1) - nokogiri (~> 1.10.4) - backburner (1.5.0) + faraday_middleware (~> 1.0, >= 1.0.0.rc1) + net-http-persistent (~> 4.0) + nokogiri (~> 1, >= 1.10.8) + backburner (1.6.1) beaneater (~> 1.0) concurrent-ruby (~> 1.0, >= 1.0.1) dante (> 0.1.5) - bcrypt (3.1.13) - bcrypt (3.1.13-java) - beaneater (1.0.0) - benchmark-ips (2.7.2) - blade (0.7.1) - activesupport (>= 3.0.0) - blade-qunit_adapter (~> 2.0.1) - coffee-script - coffee-script-source - curses (~> 1.0.0) - eventmachine - faye - sprockets (>= 3.0) - thin (>= 1.6.0) - thor (>= 0.19.1) - useragent (~> 0.16.7) - blade-qunit_adapter (2.0.1) - blade-sauce_labs_plugin (0.7.3) - childprocess - faraday - selenium-webdriver - bootsnap (1.4.5) - msgpack (~> 1.0) - bootsnap (1.4.5-java) - msgpack (~> 1.0) - builder (3.2.4) - bunny (2.14.3) - amq-protocol (~> 2.3, >= 2.3.0) - byebug (11.0.1) - capybara (3.30.0) + base64 (0.2.0) + bcrypt (3.1.20) + bcrypt_pbkdf (1.1.1) + beaneater (1.1.3) + benchmark (0.4.0) + bigdecimal (3.1.9) + bindex (0.8.1) + bootsnap (1.18.4) + msgpack (~> 1.2) + brakeman (7.0.0) + racc + builder (3.3.0) + bundler-audit (0.9.2) + bundler (>= 1.2.0, < 3) + thor (~> 1.0) + bunny (2.23.0) + amq-protocol (~> 2.3, >= 2.3.1) + sorted_set (~> 1, >= 1.0.2) + capybara (3.40.0) addressable + matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) - regexp_parser (~> 1.5) + regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - childprocess (3.0.0) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.12.2) - concurrent-ruby (1.1.6) - connection_pool (2.2.2) - cookiejar (0.3.3) - crack (0.4.3) - safe_yaml (~> 1.0.0) - crass (1.0.5) - curses (1.0.2) - daemons (1.3.1) - dalli (2.7.10) + chef-utils (18.6.2) + concurrent-ruby + childprocess (5.1.0) + logger (~> 1.5) + concurrent-ruby (1.3.4) + connection_pool (2.5.0) + crack (1.0.0) + bigdecimal + rexml + crass (1.0.6) + cssbundling-rails (1.4.1) + railties (>= 6.0.0) + dalli (3.2.8) dante (0.2.0) - declarative (0.0.10) - declarative-option (0.1.0) - delayed_job (4.1.8) - activesupport (>= 3.0, < 6.1) - delayed_job_active_record (4.1.4) - activerecord (>= 3.0, < 6.1) - delayed_job (>= 3.0, < 5) - digest-crc (0.5.1) - em-http-request (1.1.5) - addressable (>= 2.3.4) - cookiejar (!= 0.3.1) - em-socksify (>= 0.3) - eventmachine (>= 1.0.3) - http_parser.rb (>= 0.6.0) - em-socksify (0.3.2) - eventmachine (>= 1.0.0.beta.4) - erubi (1.9.0) - et-orbi (1.2.2) + dartsass-rails (0.5.1) + railties (>= 6.0.0) + sass-embedded (~> 1.63) + date (3.4.1) + debug (1.10.0) + irb (~> 1.10) + reline (>= 0.3.8) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + dotenv (3.1.7) + drb (2.2.1) + ed25519 (1.3.0) + erubi (1.13.1) + et-orbi (1.2.11) tzinfo event_emitter (0.2.6) - eventmachine (1.2.7) - execjs (2.7.0) - faraday (1.0.0) - multipart-post (>= 1.2, < 3) - faraday_middleware (1.0.0.rc1) + execjs (2.10.0) + faraday (1.10.4) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.1.0) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.1) faraday (~> 1.0) - faye (1.2.4) - cookiejar (>= 0.3.0) - em-http-request (>= 0.3.0) - eventmachine (>= 0.12.0) - faye-websocket (>= 0.9.1) - multi_json (>= 1.0.0) - rack (>= 1.0.0) - websocket-driver (>= 0.5.1) - faye-websocket (0.10.9) - eventmachine (>= 0.12.0) - websocket-driver (>= 0.5.1) - ffi (1.11.3) - ffi (1.11.3-java) - ffi (1.11.3-x64-mingw32) - ffi (1.11.3-x86-mingw32) - fugit (1.3.3) - et-orbi (~> 1.1, >= 1.1.8) - raabro (~> 1.1) - globalid (0.4.2) - activesupport (>= 4.2.0) - google-api-client (0.37.2) + ffi (1.17.1) + ffi (1.17.1-aarch64-linux-gnu) + ffi (1.17.1-aarch64-linux-musl) + ffi (1.17.1-arm-linux-gnu) + ffi (1.17.1-arm-linux-musl) + ffi (1.17.1-arm64-darwin) + ffi (1.17.1-x86_64-darwin) + ffi (1.17.1-x86_64-linux-gnu) + ffi (1.17.1-x86_64-linux-musl) + fugit (1.11.1) + et-orbi (~> 1, >= 1.2.11) + raabro (~> 1.4) + globalid (1.2.1) + activesupport (>= 6.1) + google-apis-core (0.15.1) addressable (~> 2.5, >= 2.5.1) - googleauth (~> 0.9) - httpclient (>= 2.8.1, < 3.0) + googleauth (~> 1.9) + httpclient (>= 2.8.3, < 3.a) mini_mime (~> 1.0) + mutex_m representable (~> 3.0) - retriable (>= 2.0, < 4.0) - signet (~> 0.12) - google-cloud-core (1.5.0) - google-cloud-env (~> 1.0) + retriable (>= 2.0, < 4.a) + google-apis-iamcredentials_v1 (0.22.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.49.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.7.1) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (1.3.1) - faraday (>= 0.17.3, < 2.0) - google-cloud-errors (1.0.0) - google-cloud-storage (1.25.1) - addressable (~> 2.5) + google-cloud-env (2.2.1) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.4.0) + google-cloud-storage (1.54.0) + addressable (~> 2.8) digest-crc (~> 0.4) - google-api-client (~> 0.33) - google-cloud-core (~> 1.2) - googleauth (~> 0.9) + google-apis-core (~> 0.13) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (~> 0.38) + google-cloud-core (~> 1.6) + googleauth (~> 1.9) mini_mime (~> 1.0) - googleauth (0.11.0) - faraday (>= 0.17.3, < 2.0) + google-logging-utils (0.1.0) + google-protobuf (4.29.3) + bigdecimal + rake (>= 13) + google-protobuf (4.29.3-aarch64-linux) + bigdecimal + rake (>= 13) + google-protobuf (4.29.3-arm64-darwin) + bigdecimal + rake (>= 13) + google-protobuf (4.29.3-x86_64-darwin) + bigdecimal + rake (>= 13) + google-protobuf (4.29.3-x86_64-linux) + bigdecimal + rake (>= 13) + googleauth (1.12.2) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) jwt (>= 1.4, < 3.0) - memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) - signet (~> 0.12) - hashdiff (1.0.0) - hiredis (0.6.3) - hiredis (0.6.3-java) - http_parser.rb (0.6.0) - httpclient (2.8.3) - i18n (1.7.0) + signet (>= 0.16, < 2.a) + hashdiff (1.1.2) + httpclient (2.9.0) + mutex_m + i18n (1.14.6) concurrent-ruby (~> 1.0) - image_processing (1.10.2) + image_processing (1.13.0) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - jaro_winkler (1.5.4) - jaro_winkler (1.5.4-java) - jdbc-mysql (5.1.47) - jdbc-postgres (42.2.6) - jdbc-sqlite3 (3.28.0) - jmespath (1.4.0) - json (2.3.0) - json (2.3.0-java) - jwt (2.2.1) - kindlerb (1.2.0) - mustache - nokogiri - libxml-ruby (3.1.0) - listen (3.2.1) + importmap-rails (2.1.0) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.8.0) + irb (1.14.3) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.13.0) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) + jmespath (1.6.2) + jsbundling-rails (1.3.1) + railties (>= 6.0.0) + json (2.10.2) + jwt (2.10.1) + base64 + kamal (2.4.0) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 3.1) + ed25519 (~> 1.2) + net-ssh (~> 7.3) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) + kramdown (2.5.1) + rexml (>= 3.3.9) + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + language_server-protocol (3.17.0.3) + launchy (3.0.1) + addressable (~> 2.8) + childprocess (~> 5.0) + libxml-ruby (5.0.4) + lint_roller (1.1.0) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.4.0) + logger (1.6.5) + loofah (2.24.0) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) + nokogiri (>= 1.12.0) + mail (2.8.1) mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - memoist (0.16.2) - method_source (0.9.2) - mimemagic (0.3.3) - mini_magick (4.10.1) - mini_mime (1.0.2) - mini_portile2 (2.4.0) - minitest (5.14.0) - minitest-bisect (1.5.1) + net-imap + net-pop + net-smtp + marcel (1.0.4) + matrix (0.4.2) + mdl (0.12.0) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.1) + mixlib-cli (~> 2.1, >= 2.1.1) + mixlib-config (>= 2.2.1, < 4) + mixlib-shellout + mini_magick (4.13.2) + mini_mime (1.1.5) + mini_portile2 (2.8.8) + minitest (5.25.4) + minitest-bisect (1.7.0) minitest-server (~> 1.0) path_expander (~> 1.1) - minitest-reporters (1.4.2) - ansi - builder + minitest-ci (3.4.0) + minitest (>= 5.0.6) + minitest-retry (0.2.3) minitest (>= 5.0) - ruby-progressbar - minitest-retry (0.1.9) - minitest (>= 5.0) - minitest-server (1.0.6) - minitest (~> 5.0) - mono_logger (1.1.0) - msgpack (1.3.1) - msgpack (1.3.1-java) - msgpack (1.3.1-x64-mingw32) - msgpack (1.3.1-x86-mingw32) - multi_json (1.14.1) - multipart-post (2.1.1) - mustache (1.1.1) - mustermann (1.0.3) - mysql2 (0.5.3) - mysql2 (0.5.3-x64-mingw32) - mysql2 (0.5.3-x86-mingw32) - nio4r (2.5.2) - nio4r (2.5.2-java) - nokogiri (1.10.7) - mini_portile2 (~> 2.4.0) - nokogiri (1.10.7-java) - nokogiri (1.10.7-x64-mingw32) - mini_portile2 (~> 2.4.0) - nokogiri (1.10.7-x86-mingw32) - mini_portile2 (~> 2.4.0) - os (1.0.1) - parallel (1.19.1) - parser (2.7.0.2) - ast (~> 2.4.0) - path_expander (1.1.0) - pg (1.2.3) - pg (1.2.3-x64-mingw32) - pg (1.2.3-x86-mingw32) - psych (3.1.0) - public_suffix (4.0.2) - puma (4.3.1) - nio4r (~> 2.0) - puma (4.3.1-java) + minitest-server (1.0.8) + drb (~> 2.0) + minitest (~> 5.16) + mixlib-cli (2.1.8) + mixlib-config (3.0.27) + tomlrb + mixlib-shellout (3.3.4) + chef-utils + mono_logger (1.1.2) + msgpack (1.7.5) + multi_json (1.15.0) + multipart-post (2.4.1) + mustermann (3.0.3) + ruby2_keywords (~> 0.0.1) + mutex_m (0.3.0) + mysql2 (0.5.6) + net-http-persistent (4.0.5) + connection_pool (~> 2.2) + net-imap (0.5.5) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-scp (4.0.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-smtp (0.5.1) + net-protocol + net-ssh (7.3.0) + nio4r (2.7.4) + nokogiri (1.18.1) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.18.1-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.1-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.1-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.1-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.18.1-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.1-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.1-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.1-x86_64-linux-musl) + racc (~> 1.4) + os (1.1.4) + ostruct (0.6.1) + parallel (1.26.3) + parser (3.3.6.0) + ast (~> 2.4.1) + racc + path_expander (1.1.3) + pg (1.5.9) + prism (1.3.0) + propshaft (1.1.0) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + railties (>= 7.0.0) + psych (5.2.5) + date + stringio + public_suffix (6.0.1) + puma (6.5.0) nio4r (~> 2.0) - que (0.14.3) - qunit-selenium (0.0.4) - selenium-webdriver - thor - raabro (1.1.6) - racc (1.4.16) - rack (2.2.2) - rack-cache (1.10.0) + queue_classic (4.0.0) + pg (>= 1.1, < 2.0) + raabro (1.4.0) + racc (1.8.1) + rack (3.1.8) + rack-cache (1.17.0) rack (>= 0.4) - rack-protection (2.0.7) - rack - rack-proxy (0.6.5) - rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + rack-protection (4.1.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.1.0) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - rainbow (3.0.0) - rake (13.0.1) - rb-fsevent (0.10.3) - rb-inotify (0.10.1) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + rainbow (3.1.1) + rake (13.2.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) ffi (~> 1.0) - rdoc (6.2.1) - redcarpet (3.2.3) - redis (4.1.3) - redis-namespace (1.7.0) - redis (>= 3.0.4) - regexp_parser (1.6.0) - representable (3.0.4) + rbtree (0.4.6) + rdoc (6.9.1) + psych (>= 4.0.0) + redcarpet (3.6.1) + redis (5.3.0) + redis-client (>= 0.22.0) + redis-client (0.24.0) + connection_pool + redis-namespace (1.11.0) + redis (>= 4) + regexp_parser (2.10.0) + reline (0.6.0) + io-console (~> 0.5) + representable (3.2.0) declarative (< 0.1.0) - declarative-option (< 0.2.0) + trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - resque (2.0.0) - mono_logger (~> 1.0) + resque (2.7.0) + mono_logger (~> 1) multi_json (~> 1.0) redis-namespace (~> 1.6) sinatra (>= 0.9.2) - vegas (~> 0.1.2) - resque-scheduler (4.4.0) + resque-scheduler (4.11.0) mono_logger (~> 1.0) redis (>= 3.3) - resque (>= 1.26) - rufus-scheduler (~> 3.2) + resque (>= 1.27) + rufus-scheduler (~> 3.2, != 3.3) retriable (3.1.2) - rexml (3.2.3) - rubocop (0.80.0) - jaro_winkler (~> 1.5.1) + rexml (3.4.0) + rouge (4.5.1) + rubocop (1.72.2) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) - parser (>= 2.7.0.1) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - rexml + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.38.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 1.7) - rubocop-performance (1.5.2) - rubocop (>= 0.71.0) - rubocop-rails (2.4.1) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.38.1) + parser (>= 3.3.1.0) + rubocop-md (2.0.0) + lint_roller (~> 1.1) + rubocop (>= 1.72.1) + rubocop-minitest (0.37.1) + lint_roller (~> 1.1) + rubocop (>= 1.72.1, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-packaging (0.6.0) + lint_roller (~> 1.1.0) + rubocop (>= 1.72.1, < 2.0) + rubocop-performance (1.24.0) + lint_roller (~> 1.1) + rubocop (>= 1.72.1, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rails (2.30.3) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) rack (>= 1.1) - rubocop (>= 0.72.0) - ruby-progressbar (1.10.1) - ruby-vips (2.0.17) - ffi (~> 1.9) - rubyzip (2.0.0) - rufus-scheduler (3.6.0) - fugit (~> 1.1, >= 1.1.6) - safe_yaml (1.0.5) - sass-rails (6.0.0) - sassc-rails (~> 2.1, >= 2.1.1) - sassc (2.2.1) - ffi (~> 1.9) - sassc (2.2.1-x64-mingw32) - ffi (~> 1.9) - sassc (2.2.1-x86-mingw32) - ffi (~> 1.9) - sassc-rails (2.1.2) - railties (>= 4.0.0) - sassc (>= 2.0) - sprockets (> 3.0) - sprockets-rails - tilt - sdoc (1.1.0) - rdoc (>= 5.0) - selenium-webdriver (3.142.7) - childprocess (>= 0.5, < 4.0) - rubyzip (>= 1.2.2) - semantic_range (2.3.0) - sequel (5.27.0) + rubocop (>= 1.72.1, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rails-omakase (1.0.0) + rubocop + rubocop-minitest + rubocop-performance + rubocop-rails + ruby-progressbar (1.13.0) + ruby-vips (2.2.2) + ffi (~> 1.12) + logger + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + rufus-scheduler (3.9.2) + fugit (~> 1.1, >= 1.11.1) + sass-embedded (1.83.4) + google-protobuf (~> 4.29) + rake (>= 13) + sass-embedded (1.83.4-aarch64-linux-gnu) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-aarch64-linux-musl) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-arm-linux-gnueabihf) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-arm-linux-musleabihf) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-arm64-darwin) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-x86_64-darwin) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-x86_64-linux-gnu) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-x86_64-linux-musl) + google-protobuf (~> 4.29) + securerandom (0.4.1) + selenium-webdriver (4.32.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) serverengine (2.0.7) sigdump (~> 0.2.2) - sidekiq (6.0.4) - connection_pool (>= 2.2.2) - rack (>= 2.0.0) - rack-protection (>= 2.0.0) - redis (>= 4.1.0) - sigdump (0.2.4) - signet (0.13.0) - addressable (~> 2.3) - faraday (>= 0.17.3, < 2.0) + set (1.1.2) + sidekiq (8.0.2) + connection_pool (>= 2.5.0) + json (>= 2.9.0) + logger (>= 1.6.2) + rack (>= 3.1.0) + redis-client (>= 0.23.2) + sigdump (0.2.5) + signet (0.19.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - sinatra (2.0.7) - mustermann (~> 1.0) - rack (~> 2.0) - rack-protection (= 2.0.7) + sinatra (4.1.1) + logger (>= 1.6.0) + mustermann (~> 3.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.1.1) + rack-session (>= 2.0.0, < 3) tilt (~> 2.0) sneakers (2.11.0) bunny (~> 2.12) @@ -472,140 +612,214 @@ GEM rake serverengine (~> 2.0.5) thor - sprockets (4.0.0) + solid_cable (3.0.5) + actioncable (>= 7.2) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_cache (1.0.6) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_queue (1.1.2) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11.0) + railties (>= 7.1) + thor (~> 1.3.1) + sorted_set (1.0.3) + rbtree + set (~> 1.0) + sprockets (4.2.1) concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-export (1.0.0) - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) sprockets (>= 3.0.0) - sqlite3 (1.4.2) - stackprof (0.2.15) - sucker_punch (2.1.2) + sqlite3 (2.5.0) + mini_portile2 (~> 2.8.0) + sqlite3 (2.5.0-aarch64-linux-gnu) + sqlite3 (2.5.0-aarch64-linux-musl) + sqlite3 (2.5.0-arm-linux-gnu) + sqlite3 (2.5.0-arm-linux-musl) + sqlite3 (2.5.0-arm64-darwin) + sqlite3 (2.5.0-x86_64-darwin) + sqlite3 (2.5.0-x86_64-linux-gnu) + sqlite3 (2.5.0-x86_64-linux-musl) + sshkit (1.23.2) + base64 + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct + stackprof (0.2.27) + stimulus-rails (1.3.4) + railties (>= 6.0.0) + stringio (3.1.7) + sucker_punch (3.2.0) concurrent-ruby (~> 1.0) - thin (1.7.2) - daemons (~> 1.0, >= 1.0.9) - eventmachine (~> 1.0, >= 1.0.4) - rack (>= 1, < 3) - thor (1.0.1) - tilt (2.0.10) - turbolinks (5.2.1) - turbolinks-source (~> 5.2) - turbolinks-source (5.2.0) - tzinfo (2.0.2) + tailwindcss-rails (3.2.0) + railties (>= 7.0.0) + tailwindcss-ruby + tailwindcss-ruby (3.4.17) + tailwindcss-ruby (3.4.17-aarch64-linux) + tailwindcss-ruby (3.4.17-arm-linux) + tailwindcss-ruby (3.4.17-arm64-darwin) + tailwindcss-ruby (3.4.17-x86_64-darwin) + tailwindcss-ruby (3.4.17-x86_64-linux) + terser (1.2.4) + execjs (>= 0.3.0, < 3) + thor (1.3.2) + thruster (0.1.10) + thruster (0.1.10-aarch64-linux) + thruster (0.1.10-arm64-darwin) + thruster (0.1.10-x86_64-darwin) + thruster (0.1.10-x86_64-linux) + tilt (2.6.0) + timeout (0.4.3) + tomlrb (2.0.3) + trailblazer-option (0.1.2) + trilogy (2.9.0) + tsort (0.2.0) + turbo-rails (2.0.11) + actionpack (>= 6.0.0) + railties (>= 6.0.0) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2019.3) - tzinfo (>= 1.0.0) uber (0.1.0) - uglifier (4.2.0) - execjs (>= 0.3.0, < 3) - unicode-display_width (1.6.1) - useragent (0.16.10) - vegas (0.1.11) - rack (>= 1.0.0) - w3c_validators (1.3.4) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + uri (1.0.2) + useragent (0.16.11) + w3c_validators (1.3.7) json (>= 1.8) nokogiri (~> 1.6) - wdm (0.1.1) - webdrivers (4.1.3) - nokogiri (~> 1.6) - rubyzip (>= 1.3.0) - selenium-webdriver (>= 3.0, < 4.0) - webmock (3.7.6) - addressable (>= 2.3.6) + rexml (~> 3.2) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + webmock (3.25.0) + addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webpacker (5.0.0) - activesupport (>= 5.2) - rack-proxy (>= 0.6.1) - railties (>= 5.2) - semantic_range (>= 2.3.0) - websocket (1.2.8) - websocket-driver (0.7.1) - websocket-extensions (>= 0.1.0) - websocket-driver (0.7.1-java) + webrick (1.9.1) + websocket (1.2.11) + websocket-client-simple (0.9.0) + base64 + event_emitter + mutex_m + websocket + websocket-driver (0.7.7) + base64 websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.4) + websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.2.2) + zeitwerk (2.7.1) PLATFORMS - java + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin ruby - x64-mingw32 - x86-mingw32 + x86_64-darwin + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES - activerecord-jdbcmysql-adapter (>= 1.3.0) - activerecord-jdbcpostgresql-adapter (>= 1.3.0) - activerecord-jdbcsqlite3-adapter (>= 1.3.0) aws-sdk-s3 aws-sdk-sns - azure-storage-blob + azure-storage-blob (~> 2.0) backburner bcrypt (~> 3.1.11) - benchmark-ips - blade - blade-sauce_labs_plugin bootsnap (>= 1.4.4) - byebug - capybara (>= 3.26) + brakeman + bundler-audit + capybara (>= 3.39) connection_pool - dalli - delayed_job - delayed_job_active_record + cssbundling-rails + dalli (>= 3.0.1) + dartsass-rails + debug (>= 1.1.0) google-cloud-storage (~> 1.11) - hiredis image_processing (~> 1.2) - json (>= 2.0.0) - kindlerb (~> 1.2.0) + importmap-rails (>= 1.2.3) + jbuilder + jsbundling-rails + json (>= 2.0.0, != 2.7.0) + kamal (>= 2.1.0) + launchy libxml-ruby - listen (~> 3.2) + listen (~> 3.3) + mdl (!= 0.13.0) + minitest minitest-bisect - minitest-reporters + minitest-ci minitest-retry + msgpack (>= 1.7.0) mysql2 (~> 0.5) - nokogiri (>= 1.8.1) - pg (>= 0.18.0) - psych (~> 3.0) - puma - que - queue_classic! - qunit-selenium - racc (>= 1.4.6) + nokogiri (>= 1.8.1, != 1.11.0) + pg (~> 1.3) + prism + propshaft (>= 0.1.7, != 1.0.1) + puma (>= 5.0.3) + queue_classic (>= 4.0.0) + rack (~> 3.0) rack-cache (~> 1.2) rails! - rake (>= 11.1) - redcarpet (~> 3.2.3) - redis (~> 4.0) + rake (>= 13) + rdoc (< 6.10) + redcarpet (~> 3.6.1) + redis (>= 4.0.1) redis-namespace + releaser! resque resque-scheduler rexml - rubocop (>= 0.47) + rouge + rubocop (< 1.73) + rubocop-md + rubocop-minitest + rubocop-packaging rubocop-performance rubocop-rails - sass-rails - sdoc (~> 1.1) - selenium-webdriver (>= 3.141.592) - sequel - sidekiq + rubocop-rails-omakase + rubyzip (~> 2.0) + sdoc! + selenium-webdriver (>= 4.20.0) + sidekiq (!= 8.0.3) sneakers - sprockets-export - sqlite3 (~> 1.4) + solid_cable + solid_cache + solid_queue + sprockets-rails (>= 2.0.0) + sqlite3 (>= 2.1) stackprof + stimulus-rails sucker_punch - turbolinks (~> 5) + tailwindcss-rails + terser (>= 1.1.4) + thruster + trilogy (>= 2.7.0) + turbo-rails tzinfo-data - uglifier (>= 1.3.0) - w3c_validators + uri (>= 0.13.1) + useragent + w3c_validators (~> 1.3.6) wdm (>= 0.1.0) - webdrivers + web-console webmock - webpacker (~> 5.0) - websocket-client-simple! + webrick + websocket-client-simple BUNDLED WITH - 2.1.4 + 2.6.2 diff --git a/MIT-LICENSE b/MIT-LICENSE index dbae0858e7f4f..f12cfa766c555 100644 --- a/MIT-LICENSE +++ b/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2020 David Heinemeier Hansson +Copyright (c) David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/RAILS_VERSION b/RAILS_VERSION index 6e7c238e32da6..cd8590d8d7c72 100644 --- a/RAILS_VERSION +++ b/RAILS_VERSION @@ -1 +1 @@ -6.1.0.alpha +8.1.0.alpha diff --git a/README.md b/README.md index 884f6710c29a5..d084726abf892 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,14 @@ Although most Rails models are backed by a database, models can also be ordinary Ruby classes, or Ruby classes that implement a set of interfaces as provided by the [Active Model](activemodel/README.rdoc) module. +## View layer + +The _**View layer**_ is composed of "templates" that are responsible for providing +appropriate representations of your application's resources. Templates can +come in a variety of formats, but most view templates are HTML with embedded +Ruby code (ERB files). Views are typically rendered to generate a controller response +or to generate the body of an email. In Rails, View generation is handled by [Action View](actionview/README.rdoc). + ## Controller layer The _**Controller layer**_ is responsible for handling incoming HTTP requests and @@ -32,48 +40,45 @@ In Rails, incoming requests are routed by Action Dispatch to an appropriate cont controller classes are derived from `ActionController::Base`. Action Dispatch and Action Controller are bundled together in [Action Pack](actionpack/README.rdoc). -## View layer - -The _**View layer**_ is composed of "templates" that are responsible for providing -appropriate representations of your application's resources. Templates can -come in a variety of formats, but most view templates are HTML with embedded -Ruby code (ERB files). Views are typically rendered to generate a controller response -or to generate the body of an email. In Rails, View generation is handled by [Action View](actionview/README.rdoc). - ## Frameworks and libraries [Active Record](activerecord/README.rdoc), [Active Model](activemodel/README.rdoc), [Action Pack](actionpack/README.rdoc), and [Action View](actionview/README.rdoc) can each be used independently outside Rails. -In addition to that, Rails also comes with [Action Mailer](actionmailer/README.rdoc), a library -to generate and send emails; [Action Mailbox](actionmailbox/README.md), a library to receive emails within a Rails application; -[Active Job](activejob/README.md), a framework for declaring jobs and making them run on a variety of queuing -backends; [Action Cable](actioncable/README.md), a framework to -integrate WebSockets with a Rails application; [Active Storage](activestorage/README.md), a library to attach cloud -and local files to Rails applications; [Action Text](actiontext/README.md), a library to handle rich text content; -and [Active Support](activesupport/README.rdoc), a collection -of utility classes and standard library extensions that are useful for Rails, -and may also be used independently outside Rails. + +In addition to that, Rails also comes with: + +- [Action Mailer](actionmailer/README.rdoc), a library to generate and send emails +- [Action Mailbox](actionmailbox/README.md), a library to receive emails within a Rails application +- [Active Job](activejob/README.md), a framework for declaring jobs and making them run on a variety of queuing backends +- [Action Cable](actioncable/README.md), a framework to integrate WebSockets with a Rails application +- [Active Storage](activestorage/README.md), a library to attach cloud and local files to Rails applications +- [Action Text](actiontext/README.md), a library to handle rich text content +- [Active Support](activesupport/README.rdoc), a collection of utility classes and standard library extensions that are useful for Rails, and may also be used independently outside Rails ## Getting Started 1. Install Rails at the command prompt if you haven't yet: - $ gem install rails + ```bash + $ gem install rails + ``` 2. At the command prompt, create a new Rails application: - $ rails new myapp + ```bash + $ rails new myapp + ``` where "myapp" is the application name. 3. Change directory to `myapp` and start the web server: - $ cd myapp - $ bin/rails server - + ```bash + $ cd myapp + $ bin/rails server + ``` Run with `--help` or `-h` for options. -4. Go to `http://localhost:3000` and you'll see: -"Yay! You’re on Rails!" +4. Go to `http://localhost:3000` and you'll see the Rails bootscreen with your Rails and Ruby versions. 5. Follow the guidelines to start developing your application. You may find the following resources handy: @@ -83,20 +88,14 @@ and may also be used independently outside Rails. ## Contributing -[![Code Triage Badge](https://www.codetriage.com/rails/rails/badges/users.svg)](https://www.codetriage.com/rails/rails) - We encourage you to contribute to Ruby on Rails! Please check out the [Contributing to Ruby on Rails guide](https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) for guidelines about how to proceed. [Join us!](https://contributors.rubyonrails.org) Trying to report a possible security vulnerability in Rails? Please -check out our [security policy](https://rubyonrails.org/security/) for +check out our [security policy](https://rubyonrails.org/security) for guidelines about how to proceed. -Everyone interacting in Rails and its sub-projects' codebases, issue trackers, chat rooms, and mailing lists is expected to follow the Rails [code of conduct](https://rubyonrails.org/conduct/). - -## Code Status - -[![Build Status](https://badge.buildkite.com/ab1152b6a1f6a61d3ea4ec5b3eece8d4c2b830998459c75352.svg?branch=master)](https://buildkite.com/rails/rails) +Everyone interacting in Rails and its sub-projects' codebases, issue trackers, chat rooms, and mailing lists is expected to follow the Rails [code of conduct](https://rubyonrails.org/conduct). ## License diff --git a/RELEASING_RAILS.md b/RELEASING_RAILS.md index cd850e6ddf104..eeb0c2ba29fa0 100644 --- a/RELEASING_RAILS.md +++ b/RELEASING_RAILS.md @@ -17,18 +17,6 @@ Do not release with a Red CI. You can find the CI status here: https://buildkite.com/rails/rails ``` -### Is Sam Ruby happy? If not, make him happy. - -Sam Ruby keeps a [test suite](https://github.com/rubys/awdwr) that makes -sure the code samples in his book -([Agile Web Development with Rails](https://pragprog.com/book/rails51/agile-web-development-with-rails-51)) -all work. These are valuable system tests -for Rails. You can check the status of these tests here: - -[https://intertwingly.net/projects/dashboard.html](https://intertwingly.net/projects/dashboard.html) - -Do not release with Red AWDwR tests. - ### Do we have any Git dependencies? If so, contact those authors. Having Git dependencies indicates that we depend on unreleased code. @@ -36,49 +24,9 @@ Obviously Rails cannot be released when it depends on unreleased code. Contact the authors of those particular gems and work out a release date that suits them. -### Contact the security team (either tenderlove or rafaelfranca) - -Let them know of your plans to release. There may be security issues to be -addressed, and that can impact your release date. - -### Notify implementors. - -Ruby implementors have high stakes in making sure Rails works. Be kind and -give them a heads up that Rails will be released soonish. - -This is only required for major and minor releases, bugfix releases aren't a -big enough deal, and are supposed to be backward compatible. - -Send an email just giving a heads up about the upcoming release to these -lists: - -* team@jruby.org -* community@rubini.us -* rubyonrails-core@googlegroups.com - -Implementors will love you and help you. - -## 3 Days before release - -This is when you should release the release candidate. Here are your tasks -for today: - -### Is the CI green? If not, make it green. - -### Is Sam Ruby happy? If not, make him happy. - -### Contact the security team. CVE emails must be sent on this day. - -### Create a release branch. +### Announce your plans to the rest of the team on Basecamp -From the stable branch, create a release branch. For example, if you're -releasing Rails 3.0.10, do this: - -``` -[aaron@higgins rails (3-0-stable)]$ git checkout -b 3-0-10 -Switched to a new branch '3-0-10' -[aaron@higgins rails (3-0-10)]$ -``` +Let them know of your plans to release. ### Update each CHANGELOG. @@ -93,41 +41,53 @@ You can review the commits for the 3.0.10 release like this: ``` If you're doing a stable branch release, you should also ensure that all of -the CHANGELOG entries in the stable branch are also synced to the master +the CHANGELOG entries in the stable branch are also synced to the main branch. +## Day of release + +If making multiple releases. Publish them in order from oldest to newest, to +ensure that the "greatest" version also shows up in npm and GitHub Releases as +"latest". + ### Put the new version in the RAILS_VERSION file. Include an RC number if appropriate, e.g. `6.0.0.rc1`. ### Build and test the gem. -Run `rake verify` to generate the gems and install them locally. `verify` also -generates a Rails app with a migration and boots it to smoke test with in your -browser. +Run `rake install` to generate the gems and install them locally. You can now +use the version installed locally to generate a new app and check if everything +is working as expected. This will stop you from looking silly when you push an RC to rubygems.org and then realize it is broken. -### Release to RubyGems and npm. +### Check credentials for GitHub -IMPORTANT: Several gems have JavaScript components that are released as npm -packages, so you must have Node.js installed, have an npm account (npmjs.com), -and be a package owner for `@rails/actioncable`, `@rails/actiontext`, -`@rails/activestorage`, and `@rails/ujs`. You can check this by making sure your -npm user (`npm whoami`) is listed as an owner (`npm owner ls `) of each -package. Do not release until you're set up with npm! +For GitHub run `gh auth status` to check that you are logged in (run `gh login` if not). The release task will sign the release tag. If you haven't got commit signing set up, use https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work as a guide. You can generate keys with the GPG suite from here: https://gpgtools.org. -Run `rake changelog:header` to put a header with the new version in every -CHANGELOG. Don't commit this, the release task handles it. +Run `rake prep_release` to prepare the release. This will populate the gemspecs and +npm package.json with the current RAILS_VERSION, add the header to the CHANGELOGs, +build the gems, and check if bundler can resolve the dependencies. + +You can now inspect the results in the diff and see if you are happy with the +changes. + +To release, Run `rake release`. This will commit the changes, tag it, and create a GitHub +release with the proper release notes in draft mode. + +Open the corresponding GitHub release draft and check that the release notes +are correct. If everything is fine, publish the release. -Run `rake release`. This will populate the gemspecs and npm package.json with -the current RAILS_VERSION, commit the changes, tag it, and push the gems to -rubygems.org. +### Publish the gems + +To publish the gems approve the [Release workflow in GitHub Actions](https://github.com/rails/rails/actions/workflows/release.yml), +that was created after the release was published. ### Send Rails release announcements @@ -135,9 +95,8 @@ Write a release announcement that includes the version, changes, and links to GitHub where people can find the specific commit list. Here are the mailing lists where you should announce: -* rubyonrails-core@googlegroups.com -* rubyonrails-talk@googlegroups.com -* ruby-talk@ruby-lang.org +* [rubyonrails-core](https://discuss.rubyonrails.org/c/rubyonrails-core) +* [rubyonrails-talk](https://discuss.rubyonrails.org/c/rubyonrails-talk) Use Markdown format for your announcement. Remember to ask people to report issues with the release candidate to the rails-core mailing list. @@ -155,44 +114,33 @@ break existing applications. ### Post the announcement to the Rails blog. -If you used Markdown format for your email, you can just paste it into the -blog. - -* https://weblog.rubyonrails.org - -### Post the announcement to the Rails Twitter account. - -## Time between release candidate and actual release +The blog at https://rubyonrails.org/blog is built from +https://github.com/rails/website. -Check the rails-core mailing list and the GitHub issue list for regressions in -the RC. +Create a file named like +`_posts/$(date +'%F')-Rails--have-been-released.markdown` -If any regressions are found, fix the regressions and repeat the release -candidate process. We will not release the final until 72 hours after the -last release candidate has been pushed. This means that if users find -regressions, the scheduled release date must be postponed. - -When you fix the regressions, do not create a new branch. Fix them on the -stable branch, then cherry pick the commit to your release branch. No other -commits should be added to the release branch besides regression fixing commits. +Add YAML frontmatter +``` +--- +layout: post +title: 'Rails have been released!' +categories: releases +author: +published: true +date: +--- +``` -## Day of release +Use the markdown generated by `rake announce` earlier as a base for the post. +Add some context for users as to the purpose of this release (bugfix/security). -Many of these steps are the same as for the release candidate, so if you need -more explanation on a particular step, see the RC steps. +If this is a part of the latest release series, update `_data/version.yml` so +that the homepage points to the latest version. -Today, do this stuff in this order: +### Post the announcement to the Rails X account. -* Apply security patches to the release branch -* Update CHANGELOG with security fixes -* Update RAILS_VERSION to remove the rc -* Build and test the gem -* Release the gems -* If releasing a new stable version: - - Trigger stable docs generation (see below) - - Update the version in the home page -* Email security lists -* Email general announcement lists +## Security releases ### Emailing the Rails security announce list @@ -210,7 +158,7 @@ and links to each patch. Some people may not be able to upgrade right away, so we need to give them the security fixes in patch form. * Blog announcements -* Twitter announcements +* X announcements * Merge the release branch to the stable branch * Drink beer (or other cocktail) diff --git a/Rakefile b/Rakefile index a67f8fd028b26..a99fdeb5144da 100644 --- a/Rakefile +++ b/Rakefile @@ -5,27 +5,16 @@ require "net/http" $:.unshift __dir__ require "tasks/release" require "railties/lib/rails/api/task" - -desc "Build gem files for all projects" -task build: "all:build" - -desc "Build, install and verify the gem files in a generated Rails app." -task verify: "all:verify" - -desc "Prepare the release" -task prep_release: "all:prep_release" - -desc "Release all gems to rubygems and create a tag" -task release: "all:release" +require "tools/preview_docs" desc "Run all tests by default" task default: %w(test test:isolated) -%w(test test:isolated package gem).each do |task_name| +%w(test test:isolated).each do |task_name| desc "Run #{task_name} task for all projects" task task_name do errors = [] - FRAMEWORKS.each do |project| + Releaser::FRAMEWORKS.each do |project| system(%(cd #{project} && #{$0} #{task_name} --trace)) || errors << project end fail("Errors in #{errors.join(', ')}") unless errors.empty? @@ -33,15 +22,25 @@ task default: %w(test test:isolated) end desc "Smoke-test all projects" -task :smoke do - (FRAMEWORKS - %w(activerecord)).each do |project| - system %(cd #{project} && #{$0} test:isolated --trace) +task :smoke, [:frameworks, :isolated] do |task, args| + frameworks = args[:frameworks] ? args[:frameworks].split(" ") : Releaser::FRAMEWORKS + # The arguments are positional, and users may want to specify only the isolated flag.. so we allow 'all' as a default for the first argument: + if frameworks.include?("all") + frameworks = Releaser::FRAMEWORKS + end + + isolated = args[:isolated].nil? ? true : args[:isolated] == "true" + test_task = isolated ? "test:isolated" : "test" + + (frameworks - ["activerecord"]).each do |project| + system %(cd #{project} && #{$0} #{test_task} --trace) end - system %(cd activerecord && #{$0} sqlite3:isolated_test --trace) -end -desc "Install gems for all projects." -task install: "all:install" + if frameworks.include? "activerecord" + test_task = isolated ? "sqlite3:isolated_test" : "sqlite3:test" + system %(cd activerecord && #{$0} #{test_task} --trace) + end +end desc "Generate documentation for the Rails framework" if ENV["EDGE"] @@ -50,8 +49,19 @@ else Rails::API::StableTask.new("rdoc") end -desc "Bump all versions to match RAILS_VERSION" -task update_versions: "all:update_versions" +desc "Generate documentation for previewing" +task :preview_docs do + FileUtils.mkdir_p("preview") + PreviewDocs.new.render("preview") + + require "guides/rails_guides" + Rake::Task[:rdoc].invoke + + FileUtils.mv("doc/rdoc", "preview/api") + FileUtils.mv("guides/output", "preview/guides") + + system("tar -czf preview.tar.gz -C preview .") +end # We have a webhook configured in GitHub that gets invoked after pushes. # This hook triggers the following tasks: diff --git a/actioncable/.eslintrc b/actioncable/.eslintrc index 3d9ecd4bce5eb..b85ef26b314cd 100644 --- a/actioncable/.eslintrc +++ b/actioncable/.eslintrc @@ -3,7 +3,8 @@ "rules": { "semi": ["error", "never"], "quotes": ["error", "double"], - "no-unused-vars": ["error", { "vars": "all", "args": "none" }] + "no-unused-vars": ["error", { "vars": "all", "args": "none" }], + "no-console": "off" }, "plugins": [ "import" diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index c504e7058a28b..e6cd9e82e5eb6 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,38 +1,5 @@ -* `ActionCable::Connection::Base` now allows intercepting unhandled exceptions - with `rescue_from` before they are logged, which is useful for error reporting - tools and other integrations. +* Allow setting nil as subscription connection identifier for Redis. - *Justin Talbott* + *Nguyen Nguyen* -* Add `ActionCable::Channel#stream_or_reject_for` to stream if record is present, otherwise reject the connection - - *Atul Bhosale* - -* Add `ActionCable::Channel#stop_stream_from` and `#stop_stream_for` to unsubscribe from a specific stream. - - *Zhang Kang* - -* Add PostgreSQL subscription connection identificator. - - Now you can distinguish Action Cable PostgreSQL subscription connections among others. - Also, you can set custom `id` in `cable.yml` configuration. - - ```sql - SELECT application_name FROM pg_stat_activity; - /* - application_name - ------------------------ - psql - ActionCable-PID-42 - (2 rows) - */ - ``` - - *Sergey Ponomarev* - -* Subscription confirmations and rejections are now logged at the `DEBUG` level instead of `INFO`. - - *DHH* - - -Please check [6-0-stable](https://github.com/rails/rails/blob/6-0-stable/actioncable/CHANGELOG.md) for previous changes. +Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/actioncable/CHANGELOG.md) for previous changes. diff --git a/actioncable/MIT-LICENSE b/actioncable/MIT-LICENSE index a9b33b387d329..be1075927c6b6 100644 --- a/actioncable/MIT-LICENSE +++ b/actioncable/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015-2020 Basecamp, LLC +Copyright (c) 37signals LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actioncable/README.md b/actioncable/README.md index 8864e4e0aede5..418c55bdc3b96 100644 --- a/actioncable/README.md +++ b/actioncable/README.md @@ -1,13 +1,13 @@ -# Action Cable – Integrated WebSockets for Rails +# Action Cable – Integrated WebSockets for \Rails -Action Cable seamlessly integrates WebSockets with the rest of your Rails application. +Action Cable seamlessly integrates WebSockets with the rest of your \Rails application. It allows for real-time features to be written in Ruby in the same style -and form as the rest of your Rails application, while still being performant +and form as the rest of your \Rails application, while still being performant and scalable. It's a full-stack offering that provides both a client-side JavaScript framework and a server-side Ruby framework. You have access to your full domain model written with Active Record or your ORM of choice. -You can read more about Action Cable in the [Action Cable Overview](https://edgeguides.rubyonrails.org/action_cable_overview.html) guide. +You can read more about Action Cable in the [Action Cable Overview](https://guides.rubyonrails.org/action_cable_overview.html) guide. ## Support @@ -15,7 +15,7 @@ API documentation is at: * https://api.rubyonrails.org -Bug reports for the Ruby on Rails project can be filed here: +Bug reports for the Ruby on \Rails project can be filed here: * https://github.com/rails/rails/issues diff --git a/actioncable/Rakefile b/actioncable/Rakefile index d61d38d8e7b06..1b766a1162a0d 100644 --- a/actioncable/Rakefile +++ b/actioncable/Rakefile @@ -8,13 +8,14 @@ require "action_cable" task default: :test -task :package +ENV["RAILS_MINITEST_PLUGIN"] = "true" Rake::TestTask.new do |t| t.libs << "test" t.test_files = FileList["#{__dir__}/test/**/*_test.rb"] t.warning = true t.verbose = true + t.options = "--profile" if ENV["CI"] t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end diff --git a/actioncable/actioncable.gemspec b/actioncable/actioncable.gemspec index e15c836648c69..132362da296eb 100644 --- a/actioncable/actioncable.gemspec +++ b/actioncable/actioncable.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.summary = "WebSocket framework for Rails." s.description = "Structure many real-time application concerns into channels over a single WebSocket connection." - s.required_ruby_version = ">= 2.5.0" + s.required_ruby_version = ">= 3.2.0" s.license = "MIT" @@ -17,7 +17,7 @@ Gem::Specification.new do |s| s.email = ["pratiknaik@gmail.com", "david@loudthinking.com"] s.homepage = "https://rubyonrails.org" - s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/assets/javascripts/action_cable.js"] + s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/assets/javascripts/*.js"] s.require_path = "lib" s.metadata = { @@ -26,6 +26,7 @@ Gem::Specification.new do |s| "documentation_uri" => "https://api.rubyonrails.org/v#{version}/", "mailing_list_uri" => "https://discuss.rubyonrails.org/c/rubyonrails-talk", "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actioncable", + "rubygems_mfa_required" => "true", } # NOTE: Please read our dependency guidelines before updating versions: @@ -36,4 +37,5 @@ Gem::Specification.new do |s| s.add_dependency "nio4r", "~> 2.0" s.add_dependency "websocket-driver", ">= 0.6.1" + s.add_dependency "zeitwerk", "~> 2.6" end diff --git a/actioncable/app/assets/javascripts/.gitattributes b/actioncable/app/assets/javascripts/.gitattributes new file mode 100644 index 0000000000000..7051e4979a08d --- /dev/null +++ b/actioncable/app/assets/javascripts/.gitattributes @@ -0,0 +1,3 @@ +actioncable.js linguist-generated +actioncable.esm.js linguist-generated +action_cable.js linguist-generated diff --git a/actioncable/app/assets/javascripts/action_cable.js b/actioncable/app/assets/javascripts/action_cable.js index 8d14edbde1aa2..a8a2a22bd9def 100644 --- a/actioncable/app/assets/javascripts/action_cable.js +++ b/actioncable/app/assets/javascripts/action_cable.js @@ -1,154 +1,114 @@ (function(global, factory) { - typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : factory(global.ActionCable = {}); -})(this, function(exports) { + typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, + factory(global.ActionCable = {})); +})(this, (function(exports) { "use strict"; var adapters = { - logger: self.console, - WebSocket: self.WebSocket + logger: typeof console !== "undefined" ? console : undefined, + WebSocket: typeof WebSocket !== "undefined" ? WebSocket : undefined }; var logger = { - log: function log() { + log(...messages) { if (this.enabled) { - var _adapters$logger; - for (var _len = arguments.length, messages = Array(_len), _key = 0; _key < _len; _key++) { - messages[_key] = arguments[_key]; - } messages.push(Date.now()); - (_adapters$logger = adapters.logger).log.apply(_adapters$logger, [ "[ActionCable]" ].concat(messages)); - } - } - }; - var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) { - return typeof obj; - } : function(obj) { - return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; - }; - var classCallCheck = function(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } - }; - var createClass = function() { - function defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); + adapters.logger.log("[ActionCable]", ...messages); } } - return function(Constructor, protoProps, staticProps) { - if (protoProps) defineProperties(Constructor.prototype, protoProps); - if (staticProps) defineProperties(Constructor, staticProps); - return Constructor; - }; - }(); - var now = function now() { - return new Date().getTime(); - }; - var secondsSince = function secondsSince(time) { - return (now() - time) / 1e3; - }; - var clamp = function clamp(number, min, max) { - return Math.max(min, Math.min(max, number)); }; - var ConnectionMonitor = function() { - function ConnectionMonitor(connection) { - classCallCheck(this, ConnectionMonitor); + const now = () => (new Date).getTime(); + const secondsSince = time => (now() - time) / 1e3; + class ConnectionMonitor { + constructor(connection) { this.visibilityDidChange = this.visibilityDidChange.bind(this); this.connection = connection; this.reconnectAttempts = 0; } - ConnectionMonitor.prototype.start = function start() { + start() { if (!this.isRunning()) { this.startedAt = now(); delete this.stoppedAt; this.startPolling(); addEventListener("visibilitychange", this.visibilityDidChange); - logger.log("ConnectionMonitor started. pollInterval = " + this.getPollInterval() + " ms"); + logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`); } - }; - ConnectionMonitor.prototype.stop = function stop() { + } + stop() { if (this.isRunning()) { this.stoppedAt = now(); this.stopPolling(); removeEventListener("visibilitychange", this.visibilityDidChange); logger.log("ConnectionMonitor stopped"); } - }; - ConnectionMonitor.prototype.isRunning = function isRunning() { + } + isRunning() { return this.startedAt && !this.stoppedAt; - }; - ConnectionMonitor.prototype.recordPing = function recordPing() { + } + recordMessage() { this.pingedAt = now(); - }; - ConnectionMonitor.prototype.recordConnect = function recordConnect() { + } + recordConnect() { this.reconnectAttempts = 0; - this.recordPing(); delete this.disconnectedAt; logger.log("ConnectionMonitor recorded connect"); - }; - ConnectionMonitor.prototype.recordDisconnect = function recordDisconnect() { + } + recordDisconnect() { this.disconnectedAt = now(); logger.log("ConnectionMonitor recorded disconnect"); - }; - ConnectionMonitor.prototype.startPolling = function startPolling() { + } + startPolling() { this.stopPolling(); this.poll(); - }; - ConnectionMonitor.prototype.stopPolling = function stopPolling() { + } + stopPolling() { clearTimeout(this.pollTimeout); - }; - ConnectionMonitor.prototype.poll = function poll() { - var _this = this; - this.pollTimeout = setTimeout(function() { - _this.reconnectIfStale(); - _this.poll(); - }, this.getPollInterval()); - }; - ConnectionMonitor.prototype.getPollInterval = function getPollInterval() { - var _constructor$pollInte = this.constructor.pollInterval, min = _constructor$pollInte.min, max = _constructor$pollInte.max, multiplier = _constructor$pollInte.multiplier; - var interval = multiplier * Math.log(this.reconnectAttempts + 1); - return Math.round(clamp(interval, min, max) * 1e3); - }; - ConnectionMonitor.prototype.reconnectIfStale = function reconnectIfStale() { + } + poll() { + this.pollTimeout = setTimeout((() => { + this.reconnectIfStale(); + this.poll(); + }), this.getPollInterval()); + } + getPollInterval() { + const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor; + const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10)); + const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate; + const jitter = jitterMax * Math.random(); + return staleThreshold * 1e3 * backoff * (1 + jitter); + } + reconnectIfStale() { if (this.connectionIsStale()) { - logger.log("ConnectionMonitor detected stale connection. reconnectAttempts = " + this.reconnectAttempts + ", pollInterval = " + this.getPollInterval() + " ms, time disconnected = " + secondsSince(this.disconnectedAt) + " s, stale threshold = " + this.constructor.staleThreshold + " s"); + logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`); this.reconnectAttempts++; if (this.disconnectedRecently()) { - logger.log("ConnectionMonitor skipping reopening recent disconnect"); + logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`); } else { logger.log("ConnectionMonitor reopening"); this.connection.reopen(); } } - }; - ConnectionMonitor.prototype.connectionIsStale = function connectionIsStale() { - return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold; - }; - ConnectionMonitor.prototype.disconnectedRecently = function disconnectedRecently() { + } + get refreshedAt() { + return this.pingedAt ? this.pingedAt : this.startedAt; + } + connectionIsStale() { + return secondsSince(this.refreshedAt) > this.constructor.staleThreshold; + } + disconnectedRecently() { return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold; - }; - ConnectionMonitor.prototype.visibilityDidChange = function visibilityDidChange() { - var _this2 = this; + } + visibilityDidChange() { if (document.visibilityState === "visible") { - setTimeout(function() { - if (_this2.connectionIsStale() || !_this2.connection.isOpen()) { - logger.log("ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = " + document.visibilityState); - _this2.connection.reopen(); + setTimeout((() => { + if (this.connectionIsStale() || !this.connection.isOpen()) { + logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`); + this.connection.reopen(); } - }, 200); + }), 200); } - }; - return ConnectionMonitor; - }(); - ConnectionMonitor.pollInterval = { - min: 3, - max: 30, - multiplier: 5 - }; + } + } ConnectionMonitor.staleThreshold = 6; + ConnectionMonitor.reconnectionBackoffRate = .15; var INTERNAL = { message_types: { welcome: "welcome", @@ -160,138 +120,151 @@ disconnect_reasons: { unauthorized: "unauthorized", invalid_request: "invalid_request", - server_restart: "server_restart" + server_restart: "server_restart", + remote: "remote" }, default_mount_path: "/cable", protocols: [ "actioncable-v1-json", "actioncable-unsupported" ] }; - var message_types = INTERNAL.message_types, protocols = INTERNAL.protocols; - var supportedProtocols = protocols.slice(0, protocols.length - 1); - var indexOf = [].indexOf; - var Connection = function() { - function Connection(consumer) { - classCallCheck(this, Connection); + const {message_types: message_types, protocols: protocols} = INTERNAL; + const supportedProtocols = protocols.slice(0, protocols.length - 1); + const indexOf = [].indexOf; + class Connection { + constructor(consumer) { this.open = this.open.bind(this); this.consumer = consumer; this.subscriptions = this.consumer.subscriptions; this.monitor = new ConnectionMonitor(this); this.disconnected = true; } - Connection.prototype.send = function send(data) { + send(data) { if (this.isOpen()) { this.webSocket.send(JSON.stringify(data)); return true; } else { return false; } - }; - Connection.prototype.open = function open() { + } + open() { if (this.isActive()) { - logger.log("Attempted to open WebSocket, but existing socket is " + this.getState()); + logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`); return false; } else { - logger.log("Opening WebSocket, current state is " + this.getState() + ", subprotocols: " + protocols); + const socketProtocols = [ ...protocols, ...this.consumer.subprotocols || [] ]; + logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${socketProtocols}`); if (this.webSocket) { this.uninstallEventHandlers(); } - this.webSocket = new adapters.WebSocket(this.consumer.url, protocols); + this.webSocket = new adapters.WebSocket(this.consumer.url, socketProtocols); this.installEventHandlers(); this.monitor.start(); return true; } - }; - Connection.prototype.close = function close() { - var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { - allowReconnect: true - }, allowReconnect = _ref.allowReconnect; + } + close({allowReconnect: allowReconnect} = { + allowReconnect: true + }) { if (!allowReconnect) { this.monitor.stop(); } - if (this.isActive()) { + if (this.isOpen()) { return this.webSocket.close(); } - }; - Connection.prototype.reopen = function reopen() { - logger.log("Reopening WebSocket, current state is " + this.getState()); + } + reopen() { + logger.log(`Reopening WebSocket, current state is ${this.getState()}`); if (this.isActive()) { try { return this.close(); } catch (error) { logger.log("Failed to reopen WebSocket", error); } finally { - logger.log("Reopening WebSocket in " + this.constructor.reopenDelay + "ms"); + logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`); setTimeout(this.open, this.constructor.reopenDelay); } } else { return this.open(); } - }; - Connection.prototype.getProtocol = function getProtocol() { + } + getProtocol() { if (this.webSocket) { return this.webSocket.protocol; } - }; - Connection.prototype.isOpen = function isOpen() { + } + isOpen() { return this.isState("open"); - }; - Connection.prototype.isActive = function isActive() { + } + isActive() { return this.isState("open", "connecting"); - }; - Connection.prototype.isProtocolSupported = function isProtocolSupported() { + } + triedToReconnect() { + return this.monitor.reconnectAttempts > 0; + } + isProtocolSupported() { return indexOf.call(supportedProtocols, this.getProtocol()) >= 0; - }; - Connection.prototype.isState = function isState() { - for (var _len = arguments.length, states = Array(_len), _key = 0; _key < _len; _key++) { - states[_key] = arguments[_key]; - } + } + isState(...states) { return indexOf.call(states, this.getState()) >= 0; - }; - Connection.prototype.getState = function getState() { + } + getState() { if (this.webSocket) { - for (var state in adapters.WebSocket) { + for (let state in adapters.WebSocket) { if (adapters.WebSocket[state] === this.webSocket.readyState) { return state.toLowerCase(); } } } return null; - }; - Connection.prototype.installEventHandlers = function installEventHandlers() { - for (var eventName in this.events) { - var handler = this.events[eventName].bind(this); - this.webSocket["on" + eventName] = handler; + } + installEventHandlers() { + for (let eventName in this.events) { + const handler = this.events[eventName].bind(this); + this.webSocket[`on${eventName}`] = handler; } - }; - Connection.prototype.uninstallEventHandlers = function uninstallEventHandlers() { - for (var eventName in this.events) { - this.webSocket["on" + eventName] = function() {}; + } + uninstallEventHandlers() { + for (let eventName in this.events) { + this.webSocket[`on${eventName}`] = function() {}; } - }; - return Connection; - }(); + } + } Connection.reopenDelay = 500; Connection.prototype.events = { - message: function message(event) { + message(event) { if (!this.isProtocolSupported()) { return; } - var _JSON$parse = JSON.parse(event.data), identifier = _JSON$parse.identifier, message = _JSON$parse.message, reason = _JSON$parse.reason, reconnect = _JSON$parse.reconnect, type = _JSON$parse.type; + const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data); + this.monitor.recordMessage(); switch (type) { case message_types.welcome: + if (this.triedToReconnect()) { + this.reconnectAttempted = true; + } this.monitor.recordConnect(); return this.subscriptions.reload(); case message_types.disconnect: - logger.log("Disconnecting. Reason: " + reason); + logger.log(`Disconnecting. Reason: ${reason}`); return this.close({ allowReconnect: reconnect }); case message_types.ping: - return this.monitor.recordPing(); + return null; case message_types.confirmation: - return this.subscriptions.notify(identifier, "connected"); + this.subscriptions.confirmSubscription(identifier); + if (this.reconnectAttempted) { + this.reconnectAttempted = false; + return this.subscriptions.notify(identifier, "connected", { + reconnected: true + }); + } else { + return this.subscriptions.notify(identifier, "connected", { + reconnected: false + }); + } case message_types.rejection: return this.subscriptions.reject(identifier); @@ -300,8 +273,8 @@ return this.subscriptions.notify(identifier, "received", message); } }, - open: function open() { - logger.log("WebSocket onopen event, using '" + this.getProtocol() + "' subprotocol"); + open() { + logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`); this.disconnected = false; if (!this.isProtocolSupported()) { logger.log("Protocol is unsupported. Stopping monitor and disconnecting."); @@ -310,7 +283,7 @@ }); } }, - close: function close(event) { + close(event) { logger.log("WebSocket onclose event"); if (this.disconnected) { return; @@ -321,167 +294,187 @@ willAttemptReconnect: this.monitor.isRunning() }); }, - error: function error() { + error() { logger.log("WebSocket onerror event"); } }; - var extend = function extend(object, properties) { + const extend = function(object, properties) { if (properties != null) { - for (var key in properties) { - var value = properties[key]; + for (let key in properties) { + const value = properties[key]; object[key] = value; } } return object; }; - var Subscription = function() { - function Subscription(consumer) { - var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - var mixin = arguments[2]; - classCallCheck(this, Subscription); + class Subscription { + constructor(consumer, params = {}, mixin) { this.consumer = consumer; this.identifier = JSON.stringify(params); extend(this, mixin); } - Subscription.prototype.perform = function perform(action) { - var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + perform(action, data = {}) { data.action = action; return this.send(data); - }; - Subscription.prototype.send = function send(data) { + } + send(data) { return this.consumer.send({ command: "message", identifier: this.identifier, data: JSON.stringify(data) }); - }; - Subscription.prototype.unsubscribe = function unsubscribe() { + } + unsubscribe() { return this.consumer.subscriptions.remove(this); - }; - return Subscription; - }(); - var Subscriptions = function() { - function Subscriptions(consumer) { - classCallCheck(this, Subscriptions); + } + } + class SubscriptionGuarantor { + constructor(subscriptions) { + this.subscriptions = subscriptions; + this.pendingSubscriptions = []; + } + guarantee(subscription) { + if (this.pendingSubscriptions.indexOf(subscription) == -1) { + logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`); + this.pendingSubscriptions.push(subscription); + } else { + logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`); + } + this.startGuaranteeing(); + } + forget(subscription) { + logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`); + this.pendingSubscriptions = this.pendingSubscriptions.filter((s => s !== subscription)); + } + startGuaranteeing() { + this.stopGuaranteeing(); + this.retrySubscribing(); + } + stopGuaranteeing() { + clearTimeout(this.retryTimeout); + } + retrySubscribing() { + this.retryTimeout = setTimeout((() => { + if (this.subscriptions && typeof this.subscriptions.subscribe === "function") { + this.pendingSubscriptions.map((subscription => { + logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`); + this.subscriptions.subscribe(subscription); + })); + } + }), 500); + } + } + class Subscriptions { + constructor(consumer) { this.consumer = consumer; + this.guarantor = new SubscriptionGuarantor(this); this.subscriptions = []; } - Subscriptions.prototype.create = function create(channelName, mixin) { - var channel = channelName; - var params = (typeof channel === "undefined" ? "undefined" : _typeof(channel)) === "object" ? channel : { + create(channelName, mixin) { + const channel = channelName; + const params = typeof channel === "object" ? channel : { channel: channel }; - var subscription = new Subscription(this.consumer, params, mixin); + const subscription = new Subscription(this.consumer, params, mixin); return this.add(subscription); - }; - Subscriptions.prototype.add = function add(subscription) { + } + add(subscription) { this.subscriptions.push(subscription); this.consumer.ensureActiveConnection(); this.notify(subscription, "initialized"); - this.sendCommand(subscription, "subscribe"); + this.subscribe(subscription); return subscription; - }; - Subscriptions.prototype.remove = function remove(subscription) { + } + remove(subscription) { this.forget(subscription); if (!this.findAll(subscription.identifier).length) { this.sendCommand(subscription, "unsubscribe"); } return subscription; - }; - Subscriptions.prototype.reject = function reject(identifier) { - var _this = this; - return this.findAll(identifier).map(function(subscription) { - _this.forget(subscription); - _this.notify(subscription, "rejected"); + } + reject(identifier) { + return this.findAll(identifier).map((subscription => { + this.forget(subscription); + this.notify(subscription, "rejected"); return subscription; - }); - }; - Subscriptions.prototype.forget = function forget(subscription) { - this.subscriptions = this.subscriptions.filter(function(s) { - return s !== subscription; - }); + })); + } + forget(subscription) { + this.guarantor.forget(subscription); + this.subscriptions = this.subscriptions.filter((s => s !== subscription)); return subscription; - }; - Subscriptions.prototype.findAll = function findAll(identifier) { - return this.subscriptions.filter(function(s) { - return s.identifier === identifier; - }); - }; - Subscriptions.prototype.reload = function reload() { - var _this2 = this; - return this.subscriptions.map(function(subscription) { - return _this2.sendCommand(subscription, "subscribe"); - }); - }; - Subscriptions.prototype.notifyAll = function notifyAll(callbackName) { - var _this3 = this; - for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - args[_key - 1] = arguments[_key]; - } - return this.subscriptions.map(function(subscription) { - return _this3.notify.apply(_this3, [ subscription, callbackName ].concat(args)); - }); - }; - Subscriptions.prototype.notify = function notify(subscription, callbackName) { - for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { - args[_key2 - 2] = arguments[_key2]; - } - var subscriptions = void 0; + } + findAll(identifier) { + return this.subscriptions.filter((s => s.identifier === identifier)); + } + reload() { + return this.subscriptions.map((subscription => this.subscribe(subscription))); + } + notifyAll(callbackName, ...args) { + return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args))); + } + notify(subscription, callbackName, ...args) { + let subscriptions; if (typeof subscription === "string") { subscriptions = this.findAll(subscription); } else { subscriptions = [ subscription ]; } - return subscriptions.map(function(subscription) { - return typeof subscription[callbackName] === "function" ? subscription[callbackName].apply(subscription, args) : undefined; - }); - }; - Subscriptions.prototype.sendCommand = function sendCommand(subscription, command) { - var identifier = subscription.identifier; + return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined)); + } + subscribe(subscription) { + if (this.sendCommand(subscription, "subscribe")) { + this.guarantor.guarantee(subscription); + } + } + confirmSubscription(identifier) { + logger.log(`Subscription confirmed ${identifier}`); + this.findAll(identifier).map((subscription => this.guarantor.forget(subscription))); + } + sendCommand(subscription, command) { + const {identifier: identifier} = subscription; return this.consumer.send({ command: command, identifier: identifier }); - }; - return Subscriptions; - }(); - var Consumer = function() { - function Consumer(url) { - classCallCheck(this, Consumer); + } + } + class Consumer { + constructor(url) { this._url = url; this.subscriptions = new Subscriptions(this); this.connection = new Connection(this); + this.subprotocols = []; + } + get url() { + return createWebSocketURL(this._url); } - Consumer.prototype.send = function send(data) { + send(data) { return this.connection.send(data); - }; - Consumer.prototype.connect = function connect() { + } + connect() { return this.connection.open(); - }; - Consumer.prototype.disconnect = function disconnect() { + } + disconnect() { return this.connection.close({ allowReconnect: false }); - }; - Consumer.prototype.ensureActiveConnection = function ensureActiveConnection() { + } + ensureActiveConnection() { if (!this.connection.isActive()) { return this.connection.open(); } - }; - createClass(Consumer, [ { - key: "url", - get: function get$$1() { - return createWebSocketURL(this._url); - } - } ]); - return Consumer; - }(); + } + addSubProtocol(subprotocol) { + this.subprotocols = [ ...this.subprotocols, subprotocol ]; + } + } function createWebSocketURL(url) { if (typeof url === "function") { url = url(); } if (url && !/^wss?:/i.test(url)) { - var a = document.createElement("a"); + const a = document.createElement("a"); a.href = url; a.href = a.href; a.protocol = a.protocol.replace("http", "ws"); @@ -490,28 +483,29 @@ return url; } } - function createConsumer() { - var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getConfig("url") || INTERNAL.default_mount_path; + function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) { return new Consumer(url); } function getConfig(name) { - var element = document.head.querySelector("meta[name='action-cable-" + name + "']"); + const element = document.head.querySelector(`meta[name='action-cable-${name}']`); if (element) { return element.getAttribute("content"); } } + console.log("DEPRECATION: action_cable.js has been renamed to actioncable.js – please update your reference before Rails 8"); exports.Connection = Connection; exports.ConnectionMonitor = ConnectionMonitor; exports.Consumer = Consumer; exports.INTERNAL = INTERNAL; exports.Subscription = Subscription; + exports.SubscriptionGuarantor = SubscriptionGuarantor; exports.Subscriptions = Subscriptions; exports.adapters = adapters; - exports.createWebSocketURL = createWebSocketURL; - exports.logger = logger; exports.createConsumer = createConsumer; + exports.createWebSocketURL = createWebSocketURL; exports.getConfig = getConfig; + exports.logger = logger; Object.defineProperty(exports, "__esModule", { value: true }); -}); +})); diff --git a/actioncable/app/assets/javascripts/actioncable.esm.js b/actioncable/app/assets/javascripts/actioncable.esm.js new file mode 100644 index 0000000000000..18320091e55e2 --- /dev/null +++ b/actioncable/app/assets/javascripts/actioncable.esm.js @@ -0,0 +1,512 @@ +var adapters = { + logger: typeof console !== "undefined" ? console : undefined, + WebSocket: typeof WebSocket !== "undefined" ? WebSocket : undefined +}; + +var logger = { + log(...messages) { + if (this.enabled) { + messages.push(Date.now()); + adapters.logger.log("[ActionCable]", ...messages); + } + } +}; + +const now = () => (new Date).getTime(); + +const secondsSince = time => (now() - time) / 1e3; + +class ConnectionMonitor { + constructor(connection) { + this.visibilityDidChange = this.visibilityDidChange.bind(this); + this.connection = connection; + this.reconnectAttempts = 0; + } + start() { + if (!this.isRunning()) { + this.startedAt = now(); + delete this.stoppedAt; + this.startPolling(); + addEventListener("visibilitychange", this.visibilityDidChange); + logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`); + } + } + stop() { + if (this.isRunning()) { + this.stoppedAt = now(); + this.stopPolling(); + removeEventListener("visibilitychange", this.visibilityDidChange); + logger.log("ConnectionMonitor stopped"); + } + } + isRunning() { + return this.startedAt && !this.stoppedAt; + } + recordMessage() { + this.pingedAt = now(); + } + recordConnect() { + this.reconnectAttempts = 0; + delete this.disconnectedAt; + logger.log("ConnectionMonitor recorded connect"); + } + recordDisconnect() { + this.disconnectedAt = now(); + logger.log("ConnectionMonitor recorded disconnect"); + } + startPolling() { + this.stopPolling(); + this.poll(); + } + stopPolling() { + clearTimeout(this.pollTimeout); + } + poll() { + this.pollTimeout = setTimeout((() => { + this.reconnectIfStale(); + this.poll(); + }), this.getPollInterval()); + } + getPollInterval() { + const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor; + const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10)); + const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate; + const jitter = jitterMax * Math.random(); + return staleThreshold * 1e3 * backoff * (1 + jitter); + } + reconnectIfStale() { + if (this.connectionIsStale()) { + logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`); + this.reconnectAttempts++; + if (this.disconnectedRecently()) { + logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`); + } else { + logger.log("ConnectionMonitor reopening"); + this.connection.reopen(); + } + } + } + get refreshedAt() { + return this.pingedAt ? this.pingedAt : this.startedAt; + } + connectionIsStale() { + return secondsSince(this.refreshedAt) > this.constructor.staleThreshold; + } + disconnectedRecently() { + return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold; + } + visibilityDidChange() { + if (document.visibilityState === "visible") { + setTimeout((() => { + if (this.connectionIsStale() || !this.connection.isOpen()) { + logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`); + this.connection.reopen(); + } + }), 200); + } + } +} + +ConnectionMonitor.staleThreshold = 6; + +ConnectionMonitor.reconnectionBackoffRate = .15; + +var INTERNAL = { + message_types: { + welcome: "welcome", + disconnect: "disconnect", + ping: "ping", + confirmation: "confirm_subscription", + rejection: "reject_subscription" + }, + disconnect_reasons: { + unauthorized: "unauthorized", + invalid_request: "invalid_request", + server_restart: "server_restart", + remote: "remote" + }, + default_mount_path: "/cable", + protocols: [ "actioncable-v1-json", "actioncable-unsupported" ] +}; + +const {message_types: message_types, protocols: protocols} = INTERNAL; + +const supportedProtocols = protocols.slice(0, protocols.length - 1); + +const indexOf = [].indexOf; + +class Connection { + constructor(consumer) { + this.open = this.open.bind(this); + this.consumer = consumer; + this.subscriptions = this.consumer.subscriptions; + this.monitor = new ConnectionMonitor(this); + this.disconnected = true; + } + send(data) { + if (this.isOpen()) { + this.webSocket.send(JSON.stringify(data)); + return true; + } else { + return false; + } + } + open() { + if (this.isActive()) { + logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`); + return false; + } else { + const socketProtocols = [ ...protocols, ...this.consumer.subprotocols || [] ]; + logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${socketProtocols}`); + if (this.webSocket) { + this.uninstallEventHandlers(); + } + this.webSocket = new adapters.WebSocket(this.consumer.url, socketProtocols); + this.installEventHandlers(); + this.monitor.start(); + return true; + } + } + close({allowReconnect: allowReconnect} = { + allowReconnect: true + }) { + if (!allowReconnect) { + this.monitor.stop(); + } + if (this.isOpen()) { + return this.webSocket.close(); + } + } + reopen() { + logger.log(`Reopening WebSocket, current state is ${this.getState()}`); + if (this.isActive()) { + try { + return this.close(); + } catch (error) { + logger.log("Failed to reopen WebSocket", error); + } finally { + logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`); + setTimeout(this.open, this.constructor.reopenDelay); + } + } else { + return this.open(); + } + } + getProtocol() { + if (this.webSocket) { + return this.webSocket.protocol; + } + } + isOpen() { + return this.isState("open"); + } + isActive() { + return this.isState("open", "connecting"); + } + triedToReconnect() { + return this.monitor.reconnectAttempts > 0; + } + isProtocolSupported() { + return indexOf.call(supportedProtocols, this.getProtocol()) >= 0; + } + isState(...states) { + return indexOf.call(states, this.getState()) >= 0; + } + getState() { + if (this.webSocket) { + for (let state in adapters.WebSocket) { + if (adapters.WebSocket[state] === this.webSocket.readyState) { + return state.toLowerCase(); + } + } + } + return null; + } + installEventHandlers() { + for (let eventName in this.events) { + const handler = this.events[eventName].bind(this); + this.webSocket[`on${eventName}`] = handler; + } + } + uninstallEventHandlers() { + for (let eventName in this.events) { + this.webSocket[`on${eventName}`] = function() {}; + } + } +} + +Connection.reopenDelay = 500; + +Connection.prototype.events = { + message(event) { + if (!this.isProtocolSupported()) { + return; + } + const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data); + this.monitor.recordMessage(); + switch (type) { + case message_types.welcome: + if (this.triedToReconnect()) { + this.reconnectAttempted = true; + } + this.monitor.recordConnect(); + return this.subscriptions.reload(); + + case message_types.disconnect: + logger.log(`Disconnecting. Reason: ${reason}`); + return this.close({ + allowReconnect: reconnect + }); + + case message_types.ping: + return null; + + case message_types.confirmation: + this.subscriptions.confirmSubscription(identifier); + if (this.reconnectAttempted) { + this.reconnectAttempted = false; + return this.subscriptions.notify(identifier, "connected", { + reconnected: true + }); + } else { + return this.subscriptions.notify(identifier, "connected", { + reconnected: false + }); + } + + case message_types.rejection: + return this.subscriptions.reject(identifier); + + default: + return this.subscriptions.notify(identifier, "received", message); + } + }, + open() { + logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`); + this.disconnected = false; + if (!this.isProtocolSupported()) { + logger.log("Protocol is unsupported. Stopping monitor and disconnecting."); + return this.close({ + allowReconnect: false + }); + } + }, + close(event) { + logger.log("WebSocket onclose event"); + if (this.disconnected) { + return; + } + this.disconnected = true; + this.monitor.recordDisconnect(); + return this.subscriptions.notifyAll("disconnected", { + willAttemptReconnect: this.monitor.isRunning() + }); + }, + error() { + logger.log("WebSocket onerror event"); + } +}; + +const extend = function(object, properties) { + if (properties != null) { + for (let key in properties) { + const value = properties[key]; + object[key] = value; + } + } + return object; +}; + +class Subscription { + constructor(consumer, params = {}, mixin) { + this.consumer = consumer; + this.identifier = JSON.stringify(params); + extend(this, mixin); + } + perform(action, data = {}) { + data.action = action; + return this.send(data); + } + send(data) { + return this.consumer.send({ + command: "message", + identifier: this.identifier, + data: JSON.stringify(data) + }); + } + unsubscribe() { + return this.consumer.subscriptions.remove(this); + } +} + +class SubscriptionGuarantor { + constructor(subscriptions) { + this.subscriptions = subscriptions; + this.pendingSubscriptions = []; + } + guarantee(subscription) { + if (this.pendingSubscriptions.indexOf(subscription) == -1) { + logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`); + this.pendingSubscriptions.push(subscription); + } else { + logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`); + } + this.startGuaranteeing(); + } + forget(subscription) { + logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`); + this.pendingSubscriptions = this.pendingSubscriptions.filter((s => s !== subscription)); + } + startGuaranteeing() { + this.stopGuaranteeing(); + this.retrySubscribing(); + } + stopGuaranteeing() { + clearTimeout(this.retryTimeout); + } + retrySubscribing() { + this.retryTimeout = setTimeout((() => { + if (this.subscriptions && typeof this.subscriptions.subscribe === "function") { + this.pendingSubscriptions.map((subscription => { + logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`); + this.subscriptions.subscribe(subscription); + })); + } + }), 500); + } +} + +class Subscriptions { + constructor(consumer) { + this.consumer = consumer; + this.guarantor = new SubscriptionGuarantor(this); + this.subscriptions = []; + } + create(channelName, mixin) { + const channel = channelName; + const params = typeof channel === "object" ? channel : { + channel: channel + }; + const subscription = new Subscription(this.consumer, params, mixin); + return this.add(subscription); + } + add(subscription) { + this.subscriptions.push(subscription); + this.consumer.ensureActiveConnection(); + this.notify(subscription, "initialized"); + this.subscribe(subscription); + return subscription; + } + remove(subscription) { + this.forget(subscription); + if (!this.findAll(subscription.identifier).length) { + this.sendCommand(subscription, "unsubscribe"); + } + return subscription; + } + reject(identifier) { + return this.findAll(identifier).map((subscription => { + this.forget(subscription); + this.notify(subscription, "rejected"); + return subscription; + })); + } + forget(subscription) { + this.guarantor.forget(subscription); + this.subscriptions = this.subscriptions.filter((s => s !== subscription)); + return subscription; + } + findAll(identifier) { + return this.subscriptions.filter((s => s.identifier === identifier)); + } + reload() { + return this.subscriptions.map((subscription => this.subscribe(subscription))); + } + notifyAll(callbackName, ...args) { + return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args))); + } + notify(subscription, callbackName, ...args) { + let subscriptions; + if (typeof subscription === "string") { + subscriptions = this.findAll(subscription); + } else { + subscriptions = [ subscription ]; + } + return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined)); + } + subscribe(subscription) { + if (this.sendCommand(subscription, "subscribe")) { + this.guarantor.guarantee(subscription); + } + } + confirmSubscription(identifier) { + logger.log(`Subscription confirmed ${identifier}`); + this.findAll(identifier).map((subscription => this.guarantor.forget(subscription))); + } + sendCommand(subscription, command) { + const {identifier: identifier} = subscription; + return this.consumer.send({ + command: command, + identifier: identifier + }); + } +} + +class Consumer { + constructor(url) { + this._url = url; + this.subscriptions = new Subscriptions(this); + this.connection = new Connection(this); + this.subprotocols = []; + } + get url() { + return createWebSocketURL(this._url); + } + send(data) { + return this.connection.send(data); + } + connect() { + return this.connection.open(); + } + disconnect() { + return this.connection.close({ + allowReconnect: false + }); + } + ensureActiveConnection() { + if (!this.connection.isActive()) { + return this.connection.open(); + } + } + addSubProtocol(subprotocol) { + this.subprotocols = [ ...this.subprotocols, subprotocol ]; + } +} + +function createWebSocketURL(url) { + if (typeof url === "function") { + url = url(); + } + if (url && !/^wss?:/i.test(url)) { + const a = document.createElement("a"); + a.href = url; + a.href = a.href; + a.protocol = a.protocol.replace("http", "ws"); + return a.href; + } else { + return url; + } +} + +function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) { + return new Consumer(url); +} + +function getConfig(name) { + const element = document.head.querySelector(`meta[name='action-cable-${name}']`); + if (element) { + return element.getAttribute("content"); + } +} + +export { Connection, ConnectionMonitor, Consumer, INTERNAL, Subscription, SubscriptionGuarantor, Subscriptions, adapters, createConsumer, createWebSocketURL, getConfig, logger }; diff --git a/actioncable/app/assets/javascripts/actioncable.js b/actioncable/app/assets/javascripts/actioncable.js new file mode 100644 index 0000000000000..5fc994339fb36 --- /dev/null +++ b/actioncable/app/assets/javascripts/actioncable.js @@ -0,0 +1,510 @@ +(function(global, factory) { + typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, + factory(global.ActionCable = {})); +})(this, (function(exports) { + "use strict"; + var adapters = { + logger: typeof console !== "undefined" ? console : undefined, + WebSocket: typeof WebSocket !== "undefined" ? WebSocket : undefined + }; + var logger = { + log(...messages) { + if (this.enabled) { + messages.push(Date.now()); + adapters.logger.log("[ActionCable]", ...messages); + } + } + }; + const now = () => (new Date).getTime(); + const secondsSince = time => (now() - time) / 1e3; + class ConnectionMonitor { + constructor(connection) { + this.visibilityDidChange = this.visibilityDidChange.bind(this); + this.connection = connection; + this.reconnectAttempts = 0; + } + start() { + if (!this.isRunning()) { + this.startedAt = now(); + delete this.stoppedAt; + this.startPolling(); + addEventListener("visibilitychange", this.visibilityDidChange); + logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`); + } + } + stop() { + if (this.isRunning()) { + this.stoppedAt = now(); + this.stopPolling(); + removeEventListener("visibilitychange", this.visibilityDidChange); + logger.log("ConnectionMonitor stopped"); + } + } + isRunning() { + return this.startedAt && !this.stoppedAt; + } + recordMessage() { + this.pingedAt = now(); + } + recordConnect() { + this.reconnectAttempts = 0; + delete this.disconnectedAt; + logger.log("ConnectionMonitor recorded connect"); + } + recordDisconnect() { + this.disconnectedAt = now(); + logger.log("ConnectionMonitor recorded disconnect"); + } + startPolling() { + this.stopPolling(); + this.poll(); + } + stopPolling() { + clearTimeout(this.pollTimeout); + } + poll() { + this.pollTimeout = setTimeout((() => { + this.reconnectIfStale(); + this.poll(); + }), this.getPollInterval()); + } + getPollInterval() { + const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor; + const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10)); + const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate; + const jitter = jitterMax * Math.random(); + return staleThreshold * 1e3 * backoff * (1 + jitter); + } + reconnectIfStale() { + if (this.connectionIsStale()) { + logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`); + this.reconnectAttempts++; + if (this.disconnectedRecently()) { + logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`); + } else { + logger.log("ConnectionMonitor reopening"); + this.connection.reopen(); + } + } + } + get refreshedAt() { + return this.pingedAt ? this.pingedAt : this.startedAt; + } + connectionIsStale() { + return secondsSince(this.refreshedAt) > this.constructor.staleThreshold; + } + disconnectedRecently() { + return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold; + } + visibilityDidChange() { + if (document.visibilityState === "visible") { + setTimeout((() => { + if (this.connectionIsStale() || !this.connection.isOpen()) { + logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`); + this.connection.reopen(); + } + }), 200); + } + } + } + ConnectionMonitor.staleThreshold = 6; + ConnectionMonitor.reconnectionBackoffRate = .15; + var INTERNAL = { + message_types: { + welcome: "welcome", + disconnect: "disconnect", + ping: "ping", + confirmation: "confirm_subscription", + rejection: "reject_subscription" + }, + disconnect_reasons: { + unauthorized: "unauthorized", + invalid_request: "invalid_request", + server_restart: "server_restart", + remote: "remote" + }, + default_mount_path: "/cable", + protocols: [ "actioncable-v1-json", "actioncable-unsupported" ] + }; + const {message_types: message_types, protocols: protocols} = INTERNAL; + const supportedProtocols = protocols.slice(0, protocols.length - 1); + const indexOf = [].indexOf; + class Connection { + constructor(consumer) { + this.open = this.open.bind(this); + this.consumer = consumer; + this.subscriptions = this.consumer.subscriptions; + this.monitor = new ConnectionMonitor(this); + this.disconnected = true; + } + send(data) { + if (this.isOpen()) { + this.webSocket.send(JSON.stringify(data)); + return true; + } else { + return false; + } + } + open() { + if (this.isActive()) { + logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`); + return false; + } else { + const socketProtocols = [ ...protocols, ...this.consumer.subprotocols || [] ]; + logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${socketProtocols}`); + if (this.webSocket) { + this.uninstallEventHandlers(); + } + this.webSocket = new adapters.WebSocket(this.consumer.url, socketProtocols); + this.installEventHandlers(); + this.monitor.start(); + return true; + } + } + close({allowReconnect: allowReconnect} = { + allowReconnect: true + }) { + if (!allowReconnect) { + this.monitor.stop(); + } + if (this.isOpen()) { + return this.webSocket.close(); + } + } + reopen() { + logger.log(`Reopening WebSocket, current state is ${this.getState()}`); + if (this.isActive()) { + try { + return this.close(); + } catch (error) { + logger.log("Failed to reopen WebSocket", error); + } finally { + logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`); + setTimeout(this.open, this.constructor.reopenDelay); + } + } else { + return this.open(); + } + } + getProtocol() { + if (this.webSocket) { + return this.webSocket.protocol; + } + } + isOpen() { + return this.isState("open"); + } + isActive() { + return this.isState("open", "connecting"); + } + triedToReconnect() { + return this.monitor.reconnectAttempts > 0; + } + isProtocolSupported() { + return indexOf.call(supportedProtocols, this.getProtocol()) >= 0; + } + isState(...states) { + return indexOf.call(states, this.getState()) >= 0; + } + getState() { + if (this.webSocket) { + for (let state in adapters.WebSocket) { + if (adapters.WebSocket[state] === this.webSocket.readyState) { + return state.toLowerCase(); + } + } + } + return null; + } + installEventHandlers() { + for (let eventName in this.events) { + const handler = this.events[eventName].bind(this); + this.webSocket[`on${eventName}`] = handler; + } + } + uninstallEventHandlers() { + for (let eventName in this.events) { + this.webSocket[`on${eventName}`] = function() {}; + } + } + } + Connection.reopenDelay = 500; + Connection.prototype.events = { + message(event) { + if (!this.isProtocolSupported()) { + return; + } + const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data); + this.monitor.recordMessage(); + switch (type) { + case message_types.welcome: + if (this.triedToReconnect()) { + this.reconnectAttempted = true; + } + this.monitor.recordConnect(); + return this.subscriptions.reload(); + + case message_types.disconnect: + logger.log(`Disconnecting. Reason: ${reason}`); + return this.close({ + allowReconnect: reconnect + }); + + case message_types.ping: + return null; + + case message_types.confirmation: + this.subscriptions.confirmSubscription(identifier); + if (this.reconnectAttempted) { + this.reconnectAttempted = false; + return this.subscriptions.notify(identifier, "connected", { + reconnected: true + }); + } else { + return this.subscriptions.notify(identifier, "connected", { + reconnected: false + }); + } + + case message_types.rejection: + return this.subscriptions.reject(identifier); + + default: + return this.subscriptions.notify(identifier, "received", message); + } + }, + open() { + logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`); + this.disconnected = false; + if (!this.isProtocolSupported()) { + logger.log("Protocol is unsupported. Stopping monitor and disconnecting."); + return this.close({ + allowReconnect: false + }); + } + }, + close(event) { + logger.log("WebSocket onclose event"); + if (this.disconnected) { + return; + } + this.disconnected = true; + this.monitor.recordDisconnect(); + return this.subscriptions.notifyAll("disconnected", { + willAttemptReconnect: this.monitor.isRunning() + }); + }, + error() { + logger.log("WebSocket onerror event"); + } + }; + const extend = function(object, properties) { + if (properties != null) { + for (let key in properties) { + const value = properties[key]; + object[key] = value; + } + } + return object; + }; + class Subscription { + constructor(consumer, params = {}, mixin) { + this.consumer = consumer; + this.identifier = JSON.stringify(params); + extend(this, mixin); + } + perform(action, data = {}) { + data.action = action; + return this.send(data); + } + send(data) { + return this.consumer.send({ + command: "message", + identifier: this.identifier, + data: JSON.stringify(data) + }); + } + unsubscribe() { + return this.consumer.subscriptions.remove(this); + } + } + class SubscriptionGuarantor { + constructor(subscriptions) { + this.subscriptions = subscriptions; + this.pendingSubscriptions = []; + } + guarantee(subscription) { + if (this.pendingSubscriptions.indexOf(subscription) == -1) { + logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`); + this.pendingSubscriptions.push(subscription); + } else { + logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`); + } + this.startGuaranteeing(); + } + forget(subscription) { + logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`); + this.pendingSubscriptions = this.pendingSubscriptions.filter((s => s !== subscription)); + } + startGuaranteeing() { + this.stopGuaranteeing(); + this.retrySubscribing(); + } + stopGuaranteeing() { + clearTimeout(this.retryTimeout); + } + retrySubscribing() { + this.retryTimeout = setTimeout((() => { + if (this.subscriptions && typeof this.subscriptions.subscribe === "function") { + this.pendingSubscriptions.map((subscription => { + logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`); + this.subscriptions.subscribe(subscription); + })); + } + }), 500); + } + } + class Subscriptions { + constructor(consumer) { + this.consumer = consumer; + this.guarantor = new SubscriptionGuarantor(this); + this.subscriptions = []; + } + create(channelName, mixin) { + const channel = channelName; + const params = typeof channel === "object" ? channel : { + channel: channel + }; + const subscription = new Subscription(this.consumer, params, mixin); + return this.add(subscription); + } + add(subscription) { + this.subscriptions.push(subscription); + this.consumer.ensureActiveConnection(); + this.notify(subscription, "initialized"); + this.subscribe(subscription); + return subscription; + } + remove(subscription) { + this.forget(subscription); + if (!this.findAll(subscription.identifier).length) { + this.sendCommand(subscription, "unsubscribe"); + } + return subscription; + } + reject(identifier) { + return this.findAll(identifier).map((subscription => { + this.forget(subscription); + this.notify(subscription, "rejected"); + return subscription; + })); + } + forget(subscription) { + this.guarantor.forget(subscription); + this.subscriptions = this.subscriptions.filter((s => s !== subscription)); + return subscription; + } + findAll(identifier) { + return this.subscriptions.filter((s => s.identifier === identifier)); + } + reload() { + return this.subscriptions.map((subscription => this.subscribe(subscription))); + } + notifyAll(callbackName, ...args) { + return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args))); + } + notify(subscription, callbackName, ...args) { + let subscriptions; + if (typeof subscription === "string") { + subscriptions = this.findAll(subscription); + } else { + subscriptions = [ subscription ]; + } + return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined)); + } + subscribe(subscription) { + if (this.sendCommand(subscription, "subscribe")) { + this.guarantor.guarantee(subscription); + } + } + confirmSubscription(identifier) { + logger.log(`Subscription confirmed ${identifier}`); + this.findAll(identifier).map((subscription => this.guarantor.forget(subscription))); + } + sendCommand(subscription, command) { + const {identifier: identifier} = subscription; + return this.consumer.send({ + command: command, + identifier: identifier + }); + } + } + class Consumer { + constructor(url) { + this._url = url; + this.subscriptions = new Subscriptions(this); + this.connection = new Connection(this); + this.subprotocols = []; + } + get url() { + return createWebSocketURL(this._url); + } + send(data) { + return this.connection.send(data); + } + connect() { + return this.connection.open(); + } + disconnect() { + return this.connection.close({ + allowReconnect: false + }); + } + ensureActiveConnection() { + if (!this.connection.isActive()) { + return this.connection.open(); + } + } + addSubProtocol(subprotocol) { + this.subprotocols = [ ...this.subprotocols, subprotocol ]; + } + } + function createWebSocketURL(url) { + if (typeof url === "function") { + url = url(); + } + if (url && !/^wss?:/i.test(url)) { + const a = document.createElement("a"); + a.href = url; + a.href = a.href; + a.protocol = a.protocol.replace("http", "ws"); + return a.href; + } else { + return url; + } + } + function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) { + return new Consumer(url); + } + function getConfig(name) { + const element = document.head.querySelector(`meta[name='action-cable-${name}']`); + if (element) { + return element.getAttribute("content"); + } + } + exports.Connection = Connection; + exports.ConnectionMonitor = ConnectionMonitor; + exports.Consumer = Consumer; + exports.INTERNAL = INTERNAL; + exports.Subscription = Subscription; + exports.SubscriptionGuarantor = SubscriptionGuarantor; + exports.Subscriptions = Subscriptions; + exports.adapters = adapters; + exports.createConsumer = createConsumer; + exports.createWebSocketURL = createWebSocketURL; + exports.getConfig = getConfig; + exports.logger = logger; + Object.defineProperty(exports, "__esModule", { + value: true + }); +})); diff --git a/actioncable/app/javascript/action_cable/adapters.js b/actioncable/app/javascript/action_cable/adapters.js index 4de8131438779..f9759de9a05dd 100644 --- a/actioncable/app/javascript/action_cable/adapters.js +++ b/actioncable/app/javascript/action_cable/adapters.js @@ -1,4 +1,4 @@ export default { - logger: self.console, - WebSocket: self.WebSocket + logger: typeof console !== "undefined" ? console : undefined, + WebSocket: typeof WebSocket !== "undefined" ? WebSocket : undefined, } diff --git a/actioncable/app/javascript/action_cable/connection.js b/actioncable/app/javascript/action_cable/connection.js index 96bac132c1828..fa32cfd5a675f 100644 --- a/actioncable/app/javascript/action_cable/connection.js +++ b/actioncable/app/javascript/action_cable/connection.js @@ -33,9 +33,10 @@ class Connection { logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`) return false } else { - logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`) + const socketProtocols = [...protocols, ...this.consumer.subprotocols || []] + logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${socketProtocols}`) if (this.webSocket) { this.uninstallEventHandlers() } - this.webSocket = new adapters.WebSocket(this.consumer.url, protocols) + this.webSocket = new adapters.WebSocket(this.consumer.url, socketProtocols) this.installEventHandlers() this.monitor.start() return true @@ -44,7 +45,8 @@ class Connection { close({allowReconnect} = {allowReconnect: true}) { if (!allowReconnect) { this.monitor.stop() } - if (this.isActive()) { + // Avoid closing websockets in a "connecting" state due to Safari 15.1+ bug. See: https://github.com/rails/rails/issues/43835#issuecomment-1002288478 + if (this.isOpen()) { return this.webSocket.close() } } @@ -80,6 +82,10 @@ class Connection { return this.isState("open", "connecting") } + triedToReconnect() { + return this.monitor.reconnectAttempts > 0 + } + // Private isProtocolSupported() { @@ -122,17 +128,27 @@ Connection.prototype.events = { message(event) { if (!this.isProtocolSupported()) { return } const {identifier, message, reason, reconnect, type} = JSON.parse(event.data) + this.monitor.recordMessage() switch (type) { case message_types.welcome: + if (this.triedToReconnect()) { + this.reconnectAttempted = true + } this.monitor.recordConnect() return this.subscriptions.reload() case message_types.disconnect: logger.log(`Disconnecting. Reason: ${reason}`) return this.close({allowReconnect: reconnect}) case message_types.ping: - return this.monitor.recordPing() + return null case message_types.confirmation: - return this.subscriptions.notify(identifier, "connected") + this.subscriptions.confirmSubscription(identifier) + if (this.reconnectAttempted) { + this.reconnectAttempted = false + return this.subscriptions.notify(identifier, "connected", {reconnected: true}) + } else { + return this.subscriptions.notify(identifier, "connected", {reconnected: false}) + } case message_types.rejection: return this.subscriptions.reject(identifier) default: diff --git a/actioncable/app/javascript/action_cable/connection_monitor.js b/actioncable/app/javascript/action_cable/connection_monitor.js index 3bcee72e8f5d4..986d1408e0ba2 100644 --- a/actioncable/app/javascript/action_cable/connection_monitor.js +++ b/actioncable/app/javascript/action_cable/connection_monitor.js @@ -7,8 +7,6 @@ const now = () => new Date().getTime() const secondsSince = time => (now() - time) / 1000 -const clamp = (number, min, max) => Math.max(min, Math.min(max, number)) - class ConnectionMonitor { constructor(connection) { this.visibilityDidChange = this.visibilityDidChange.bind(this) @@ -22,7 +20,7 @@ class ConnectionMonitor { delete this.stoppedAt this.startPolling() addEventListener("visibilitychange", this.visibilityDidChange) - logger.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`) + logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`) } } @@ -39,13 +37,12 @@ class ConnectionMonitor { return this.startedAt && !this.stoppedAt } - recordPing() { + recordMessage() { this.pingedAt = now() } recordConnect() { this.reconnectAttempts = 0 - this.recordPing() delete this.disconnectedAt logger.log("ConnectionMonitor recorded connect") } @@ -75,17 +72,19 @@ class ConnectionMonitor { } getPollInterval() { - const {min, max, multiplier} = this.constructor.pollInterval - const interval = multiplier * Math.log(this.reconnectAttempts + 1) - return Math.round(clamp(interval, min, max) * 1000) + const { staleThreshold, reconnectionBackoffRate } = this.constructor + const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10)) + const jitterMax = this.reconnectAttempts === 0 ? 1.0 : reconnectionBackoffRate + const jitter = jitterMax * Math.random() + return staleThreshold * 1000 * backoff * (1 + jitter) } reconnectIfStale() { if (this.connectionIsStale()) { - logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, pollInterval = ${this.getPollInterval()} ms, time disconnected = ${secondsSince(this.disconnectedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`) + logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`) this.reconnectAttempts++ if (this.disconnectedRecently()) { - logger.log("ConnectionMonitor skipping reopening recent disconnect") + logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`) } else { logger.log("ConnectionMonitor reopening") this.connection.reopen() @@ -93,8 +92,12 @@ class ConnectionMonitor { } } + get refreshedAt() { + return this.pingedAt ? this.pingedAt : this.startedAt + } + connectionIsStale() { - return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold + return secondsSince(this.refreshedAt) > this.constructor.staleThreshold } disconnectedRecently() { @@ -115,12 +118,7 @@ class ConnectionMonitor { } -ConnectionMonitor.pollInterval = { - min: 3, - max: 30, - multiplier: 5 -} - ConnectionMonitor.staleThreshold = 6 // Server::Connections::BEAT_INTERVAL * 2 (missed two pings) +ConnectionMonitor.reconnectionBackoffRate = 0.15 export default ConnectionMonitor diff --git a/actioncable/app/javascript/action_cable/consumer.js b/actioncable/app/javascript/action_cable/consumer.js index 5c5f7d1262b8c..71ef125a45537 100644 --- a/actioncable/app/javascript/action_cable/consumer.js +++ b/actioncable/app/javascript/action_cable/consumer.js @@ -32,6 +32,7 @@ export default class Consumer { this._url = url this.subscriptions = new Subscriptions(this) this.connection = new Connection(this) + this.subprotocols = [] } get url() { @@ -55,6 +56,10 @@ export default class Consumer { return this.connection.open() } } + + addSubProtocol(subprotocol) { + this.subprotocols = [...this.subprotocols, subprotocol] + } } export function createWebSocketURL(url) { @@ -66,7 +71,7 @@ export function createWebSocketURL(url) { const a = document.createElement("a") a.href = url // Fix populating Location properties in IE. Otherwise, protocol will be blank. - a.href = a.href + a.href = a.href // eslint-disable-line a.protocol = a.protocol.replace("http", "ws") return a.href } else { diff --git a/actioncable/app/javascript/action_cable/index.js b/actioncable/app/javascript/action_cable/index.js index 848b5631d67e2..3e650bc120bc0 100644 --- a/actioncable/app/javascript/action_cable/index.js +++ b/actioncable/app/javascript/action_cable/index.js @@ -4,6 +4,7 @@ import Consumer, { createWebSocketURL } from "./consumer" import INTERNAL from "./internal" import Subscription from "./subscription" import Subscriptions from "./subscriptions" +import SubscriptionGuarantor from "./subscription_guarantor" import adapters from "./adapters" import logger from "./logger" @@ -14,6 +15,7 @@ export { INTERNAL, Subscription, Subscriptions, + SubscriptionGuarantor, adapters, createWebSocketURL, logger, diff --git a/actioncable/app/javascript/action_cable/index_with_name_deprecation.js b/actioncable/app/javascript/action_cable/index_with_name_deprecation.js new file mode 100644 index 0000000000000..272ba7bec2221 --- /dev/null +++ b/actioncable/app/javascript/action_cable/index_with_name_deprecation.js @@ -0,0 +1,2 @@ +export * from "./index" +console.log("DEPRECATION: action_cable.js has been renamed to actioncable.js – please update your reference before Rails 8") diff --git a/actioncable/app/javascript/action_cable/internal.js b/actioncable/app/javascript/action_cable/internal.js index 9c12c8e7d82a8..a007d6f471f7a 100644 --- a/actioncable/app/javascript/action_cable/internal.js +++ b/actioncable/app/javascript/action_cable/internal.js @@ -9,7 +9,8 @@ export default { "disconnect_reasons": { "unauthorized": "unauthorized", "invalid_request": "invalid_request", - "server_restart": "server_restart" + "server_restart": "server_restart", + "remote": "remote" }, "default_mount_path": "/cable", "protocols": [ diff --git a/actioncable/app/javascript/action_cable/logger.js b/actioncable/app/javascript/action_cable/logger.js index ef4661ead1cd3..c73f5bd542fc5 100644 --- a/actioncable/app/javascript/action_cable/logger.js +++ b/actioncable/app/javascript/action_cable/logger.js @@ -1,5 +1,17 @@ import adapters from "./adapters" +// The logger is disabled by default. You can enable it with: +// +// ActionCable.logger.enabled = true +// +// Example: +// +// import * as ActionCable from '@rails/actioncable' +// +// ActionCable.logger.enabled = true +// ActionCable.logger.log('Connection Established.') +// + export default { log(...messages) { if (this.enabled) { diff --git a/actioncable/app/javascript/action_cable/subscription_guarantor.js b/actioncable/app/javascript/action_cable/subscription_guarantor.js new file mode 100644 index 0000000000000..7d6ad98fa3904 --- /dev/null +++ b/actioncable/app/javascript/action_cable/subscription_guarantor.js @@ -0,0 +1,50 @@ +import logger from "./logger" + +// Responsible for ensuring channel subscribe command is confirmed, retrying until confirmation is received. +// Internal class, not intended for direct user manipulation. + +class SubscriptionGuarantor { + constructor(subscriptions) { + this.subscriptions = subscriptions + this.pendingSubscriptions = [] + } + + guarantee(subscription) { + if(this.pendingSubscriptions.indexOf(subscription) == -1){ + logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`) + this.pendingSubscriptions.push(subscription) + } + else { + logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`) + } + this.startGuaranteeing() + } + + forget(subscription) { + logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`) + this.pendingSubscriptions = (this.pendingSubscriptions.filter((s) => s !== subscription)) + } + + startGuaranteeing() { + this.stopGuaranteeing() + this.retrySubscribing() + } + + stopGuaranteeing() { + clearTimeout(this.retryTimeout) + } + + retrySubscribing() { + this.retryTimeout = setTimeout(() => { + if (this.subscriptions && typeof(this.subscriptions.subscribe) === "function") { + this.pendingSubscriptions.map((subscription) => { + logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`) + this.subscriptions.subscribe(subscription) + }) + } + } + , 500) + } +} + +export default SubscriptionGuarantor \ No newline at end of file diff --git a/actioncable/app/javascript/action_cable/subscriptions.js b/actioncable/app/javascript/action_cable/subscriptions.js index 06ca71cd53adf..ec41ccbf75ba6 100644 --- a/actioncable/app/javascript/action_cable/subscriptions.js +++ b/actioncable/app/javascript/action_cable/subscriptions.js @@ -1,4 +1,6 @@ import Subscription from "./subscription" +import SubscriptionGuarantor from "./subscription_guarantor" +import logger from "./logger" // Collection class for creating (and internally managing) channel subscriptions. // The only method intended to be triggered by the user is ActionCable.Subscriptions#create, @@ -13,6 +15,7 @@ import Subscription from "./subscription" export default class Subscriptions { constructor(consumer) { this.consumer = consumer + this.guarantor = new SubscriptionGuarantor(this) this.subscriptions = [] } @@ -29,7 +32,7 @@ export default class Subscriptions { this.subscriptions.push(subscription) this.consumer.ensureActiveConnection() this.notify(subscription, "initialized") - this.sendCommand(subscription, "subscribe") + this.subscribe(subscription) return subscription } @@ -50,6 +53,7 @@ export default class Subscriptions { } forget(subscription) { + this.guarantor.forget(subscription) this.subscriptions = (this.subscriptions.filter((s) => s !== subscription)) return subscription } @@ -60,7 +64,7 @@ export default class Subscriptions { reload() { return this.subscriptions.map((subscription) => - this.sendCommand(subscription, "subscribe")) + this.subscribe(subscription)) } notifyAll(callbackName, ...args) { @@ -80,6 +84,18 @@ export default class Subscriptions { (typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined)) } + subscribe(subscription) { + if (this.sendCommand(subscription, "subscribe")) { + this.guarantor.guarantee(subscription) + } + } + + confirmSubscription(identifier) { + logger.log(`Subscription confirmed ${identifier}`) + this.findAll(identifier).map((subscription) => + this.guarantor.forget(subscription)) + } + sendCommand(subscription, command) { const {identifier} = subscription return this.consumer.send({command, identifier}) diff --git a/actioncable/karma.conf.js b/actioncable/karma.conf.js index 83e9c98af19c1..bb16f969546d1 100644 --- a/actioncable/karma.conf.js +++ b/actioncable/karma.conf.js @@ -25,9 +25,8 @@ if (process.env.CI) { config.customLaunchers = { sl_chrome: sauce("chrome", 70), sl_ff: sauce("firefox", 63), - sl_safari: sauce("safari", 12.0, "macOS 10.13"), + sl_safari: sauce("safari", "16", "macOS 13"), sl_edge: sauce("microsoftedge", 17.17134, "Windows 10"), - sl_ie_11: sauce("internet explorer", 11, "Windows 8.1"), } config.browsers = Object.keys(config.customLaunchers) diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb index fd48d60c28913..043e1395b1236 100644 --- a/actioncable/lib/action_cable.rb +++ b/actioncable/lib/action_cable.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2015-2020 Basecamp, LLC +# Copyright (c) 37signals LLC # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -25,10 +25,35 @@ require "active_support" require "active_support/rails" -require "action_cable/version" +require "zeitwerk" +# We compute lib this way instead of using __dir__ because __dir__ gives a real +# path, while __FILE__ honors symlinks. If the gem is stored under a symlinked +# directory, this matters. +lib = File.dirname(__FILE__) + +Zeitwerk::Loader.for_gem.tap do |loader| + loader.ignore( + "#{lib}/rails", # Contains generators, templates, docs, etc. + "#{lib}/action_cable/gem_version.rb", + "#{lib}/action_cable/version.rb", + "#{lib}/action_cable/deprecator.rb", + ) + + loader.do_not_eager_load( + "#{lib}/action_cable/subscription_adapter", # Adapters are required and loaded on demand. + "#{lib}/action_cable/test_helper.rb", + Dir["#{lib}/action_cable/**/test_case.rb"] + ) + + loader.inflector.inflect("postgresql" => "PostgreSQL") +end.setup + +# :markup: markdown +# :include: ../README.md module ActionCable - extend ActiveSupport::Autoload + require_relative "action_cable/version" + require_relative "action_cable/deprecator" INTERNAL = { message_types: { @@ -41,7 +66,8 @@ module ActionCable disconnect_reasons: { unauthorized: "unauthorized", invalid_request: "invalid_request", - server_restart: "server_restart" + server_restart: "server_restart", + remote: "remote" }, default_mount_path: "/cable", protocols: ["actioncable-v1-json", "actioncable-unsupported"].freeze @@ -51,12 +77,4 @@ module ActionCable module_function def server @server ||= ActionCable::Server::Base.new end - - autoload :Server - autoload :Connection - autoload :Channel - autoload :RemoteConnections - autoload :SubscriptionAdapter - autoload :TestHelper - autoload :TestCase end diff --git a/actioncable/lib/action_cable/channel.rb b/actioncable/lib/action_cable/channel.rb deleted file mode 100644 index d5118b9dc9d09..0000000000000 --- a/actioncable/lib/action_cable/channel.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module ActionCable - module Channel - extend ActiveSupport::Autoload - - eager_autoload do - autoload :Base - autoload :Broadcasting - autoload :Callbacks - autoload :Naming - autoload :PeriodicTimers - autoload :Streams - autoload :TestCase - end - end -end diff --git a/actioncable/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb index c90c2c4817433..89c678c712e4e 100644 --- a/actioncable/lib/action_cable/channel/base.rb +++ b/actioncable/lib/action_cable/channel/base.rb @@ -1,99 +1,111 @@ # frozen_string_literal: true -require "set" +# :markup: markdown + require "active_support/rescuable" +require "active_support/parameter_filter" module ActionCable module Channel - # The channel provides the basic structure of grouping behavior into logical units when communicating over the WebSocket connection. - # You can think of a channel like a form of controller, but one that's capable of pushing content to the subscriber in addition to simply - # responding to the subscriber's direct requests. + # # Action Cable Channel Base # - # Channel instances are long-lived. A channel object will be instantiated when the cable consumer becomes a subscriber, and then - # lives until the consumer disconnects. This may be seconds, minutes, hours, or even days. That means you have to take special care - # not to do anything silly in a channel that would balloon its memory footprint or whatever. The references are forever, so they won't be released - # as is normally the case with a controller instance that gets thrown away after every request. + # The channel provides the basic structure of grouping behavior into logical + # units when communicating over the WebSocket connection. You can think of a + # channel like a form of controller, but one that's capable of pushing content + # to the subscriber in addition to simply responding to the subscriber's direct + # requests. # - # Long-lived channels (and connections) also mean you're responsible for ensuring that the data is fresh. If you hold a reference to a user - # record, but the name is changed while that reference is held, you may be sending stale data if you don't take precautions to avoid it. + # Channel instances are long-lived. A channel object will be instantiated when + # the cable consumer becomes a subscriber, and then lives until the consumer + # disconnects. This may be seconds, minutes, hours, or even days. That means you + # have to take special care not to do anything silly in a channel that would + # balloon its memory footprint or whatever. The references are forever, so they + # won't be released as is normally the case with a controller instance that gets + # thrown away after every request. # - # The upside of long-lived channel instances is that you can use instance variables to keep reference to objects that future subscriber requests - # can interact with. Here's a quick example: + # Long-lived channels (and connections) also mean you're responsible for + # ensuring that the data is fresh. If you hold a reference to a user record, but + # the name is changed while that reference is held, you may be sending stale + # data if you don't take precautions to avoid it. # - # class ChatChannel < ApplicationCable::Channel - # def subscribed - # @room = Chat::Room[params[:room_number]] - # end + # The upside of long-lived channel instances is that you can use instance + # variables to keep reference to objects that future subscriber requests can + # interact with. Here's a quick example: + # + # class ChatChannel < ApplicationCable::Channel + # def subscribed + # @room = Chat::Room[params[:room_number]] + # end # - # def speak(data) - # @room.speak data, user: current_user + # def speak(data) + # @room.speak data, user: current_user + # end # end - # end # - # The #speak action simply uses the Chat::Room object that was created when the channel was first subscribed to by the consumer when that - # subscriber wants to say something in the room. + # The #speak action simply uses the Chat::Room object that was created when the + # channel was first subscribed to by the consumer when that subscriber wants to + # say something in the room. # - # == Action processing + # ## Action processing # # Unlike subclasses of ActionController::Base, channels do not follow a RESTful # constraint form for their actions. Instead, Action Cable operates through a - # remote-procedure call model. You can declare any public method on the - # channel (optionally taking a data argument), and this method is - # automatically exposed as callable to the client. + # remote-procedure call model. You can declare any public method on the channel + # (optionally taking a `data` argument), and this method is automatically + # exposed as callable to the client. # # Example: # - # class AppearanceChannel < ApplicationCable::Channel - # def subscribed - # @connection_token = generate_connection_token - # end - # - # def unsubscribed - # current_user.disappear @connection_token - # end + # class AppearanceChannel < ApplicationCable::Channel + # def subscribed + # @connection_token = generate_connection_token + # end # - # def appear(data) - # current_user.appear @connection_token, on: data['appearing_on'] - # end + # def unsubscribed + # current_user.disappear @connection_token + # end # - # def away - # current_user.away @connection_token - # end + # def appear(data) + # current_user.appear @connection_token, on: data['appearing_on'] + # end # - # private - # def generate_connection_token - # SecureRandom.hex(36) + # def away + # current_user.away @connection_token # end - # end # - # In this example, the subscribed and unsubscribed methods are not callable methods, as they - # were already declared in ActionCable::Channel::Base, but #appear - # and #away are. #generate_connection_token is also not - # callable, since it's a private method. You'll see that appear accepts a data - # parameter, which it then uses as part of its model call. #away - # does not, since it's simply a trigger action. + # private + # def generate_connection_token + # SecureRandom.hex(36) + # end + # end + # + # In this example, the subscribed and unsubscribed methods are not callable + # methods, as they were already declared in ActionCable::Channel::Base, but + # `#appear` and `#away` are. `#generate_connection_token` is also not callable, + # since it's a private method. You'll see that appear accepts a data parameter, + # which it then uses as part of its model call. `#away` does not, since it's + # simply a trigger action. # - # Also note that in this example, current_user is available because - # it was marked as an identifying attribute on the connection. All such - # identifiers will automatically create a delegation method of the same name - # on the channel instance. + # Also note that in this example, `current_user` is available because it was + # marked as an identifying attribute on the connection. All such identifiers + # will automatically create a delegation method of the same name on the channel + # instance. # - # == Rejecting subscription requests + # ## Rejecting subscription requests # # A channel can reject a subscription request in the #subscribed callback by # invoking the #reject method: # - # class ChatChannel < ApplicationCable::Channel - # def subscribed - # @room = Chat::Room[params[:room_number]] - # reject unless current_user.can_access?(@room) + # class ChatChannel < ApplicationCable::Channel + # def subscribed + # @room = Chat::Room[params[:room_number]] + # reject unless current_user.can_access?(@room) + # end # end - # end # - # In this example, the subscription will be rejected if the - # current_user does not have access to the chat room. On the - # client-side, the Channel#rejected callback will get invoked when - # the server rejects the subscription request. + # In this example, the subscription will be rejected if the `current_user` does + # not have access to the chat room. On the client-side, the `Channel#rejected` + # callback will get invoked when the server rejects the subscription request. class Base include Callbacks include PeriodicTimers @@ -106,14 +118,13 @@ class Base delegate :logger, to: :connection class << self - # A list of method names that should be considered actions. This - # includes all public instance methods on a channel, less - # any internal methods (defined on Base), adding back in - # any methods that are internal, but still exist on the class - # itself. + # A list of method names that should be considered actions. This includes all + # public instance methods on a channel, less any internal methods (defined on + # Base), adding back in any methods that are internal, but still exist on the + # class itself. # - # ==== Returns - # * Set - A set of all methods that should be considered actions. + # #### Returns + # * `Set` - A set of all methods that should be considered actions. def action_methods @action_methods ||= begin # All public instance methods of this class, including ancestors @@ -127,9 +138,9 @@ def action_methods end private - # action_methods are cached and there is sometimes need to refresh - # them. ::clear_action_methods! allows you to do that, so next time - # you run action_methods, they will be recalculated. + # action_methods are cached and there is sometimes need to refresh them. + # ::clear_action_methods! allows you to do that, so next time you run + # action_methods, they will be recalculated. def clear_action_methods! # :doc: @action_methods = nil end @@ -154,13 +165,14 @@ def initialize(connection, identifier, params = {}) @reject_subscription = nil @subscription_confirmation_sent = nil + @unsubscribed = false delegate_connection_identifiers end - # Extract the action name from the passed data and process it via the channel. The process will ensure - # that the action requested is a public method on the channel declared by the user (so not one of the callbacks - # like #subscribed). + # Extract the action name from the passed data and process it via the channel. + # The process will ensure that the action requested is a public method on the + # channel declared by the user (so not one of the callbacks like #subscribed). def perform_action(data) action = extract_action(data) @@ -174,8 +186,8 @@ def perform_action(data) end end - # This method is called after subscription has been added to the connection - # and confirms or rejects the subscription. + # This method is called after subscription has been added to the connection and + # confirms or rejects the subscription. def subscribe_to_channel run_callbacks :subscribe do subscribed @@ -185,33 +197,43 @@ def subscribe_to_channel ensure_confirmation_sent end - # Called by the cable connection when it's cut, so the channel has a chance to cleanup with callbacks. - # This method is not intended to be called directly by the user. Instead, overwrite the #unsubscribed callback. + # Called by the cable connection when it's cut, so the channel has a chance to + # cleanup with callbacks. This method is not intended to be called directly by + # the user. Instead, override the #unsubscribed callback. def unsubscribe_from_channel # :nodoc: + @unsubscribed = true run_callbacks :unsubscribe do unsubscribed end end + def unsubscribed? # :nodoc: + @unsubscribed + end + private - # Called once a consumer has become a subscriber of the channel. Usually the place to set up any streams - # you want this channel to be sending to the subscriber. + # Called once a consumer has become a subscriber of the channel. Usually the + # place to set up any streams you want this channel to be sending to the + # subscriber. def subscribed # :doc: # Override in subclasses end - # Called once a consumer has cut its cable connection. Can be used for cleaning up connections or marking - # users as offline or the like. + # Called once a consumer has cut its cable connection. Can be used for cleaning + # up connections or marking users as offline or the like. def unsubscribed # :doc: # Override in subclasses end - # Transmit a hash of data to the subscriber. The hash will automatically be wrapped in a JSON envelope with - # the proper channel identifier marked as the recipient. + # Transmit a hash of data to the subscriber. The hash will automatically be + # wrapped in a JSON envelope with the proper channel identifier marked as the + # recipient. def transmit(data, via: nil) # :doc: - status = "#{self.class.name} transmitting #{data.inspect.truncate(300)}" - status += " (via #{via})" if via - logger.debug(status) + logger.debug do + status = "#{self.class.name} transmitting #{data.inspect.truncate(300)}" + status += " (via #{via})" if via + status + end payload = { channel_class: self.class.name, data: data, via: via } ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do @@ -262,7 +284,7 @@ def processable_action?(action) end def dispatch_action(action, data) - logger.info action_signature(action, data) + logger.debug action_signature(action, data) if method(action).arity == 1 public_send action, data @@ -275,17 +297,24 @@ def dispatch_action(action, data) def action_signature(action, data) (+"#{self.class.name}##{action}").tap do |signature| - if (arguments = data.except("action")).any? + arguments = data.except("action") + + if arguments.any? + arguments = parameter_filter.filter(arguments) signature << "(#{arguments.inspect})" end end end + def parameter_filter + @parameter_filter ||= ActiveSupport::ParameterFilter.new(connection.config.filter_parameters) + end + def transmit_subscription_confirmation unless subscription_confirmation_sent? logger.debug "#{self.class.name} is transmitting the subscription confirmation" - ActiveSupport::Notifications.instrument("transmit_subscription_confirmation.action_cable", channel_class: self.class.name) do + ActiveSupport::Notifications.instrument("transmit_subscription_confirmation.action_cable", channel_class: self.class.name, identifier: @identifier) do connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation] @subscription_confirmation_sent = true end @@ -300,7 +329,7 @@ def reject_subscription def transmit_subscription_rejection logger.debug "#{self.class.name} is transmitting the subscription rejection" - ActiveSupport::Notifications.instrument("transmit_subscription_rejection.action_cable", channel_class: self.class.name) do + ActiveSupport::Notifications.instrument("transmit_subscription_rejection.action_cable", channel_class: self.class.name, identifier: @identifier) do connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection] end end diff --git a/actioncable/lib/action_cable/channel/broadcasting.rb b/actioncable/lib/action_cable/channel/broadcasting.rb index 9f702e425e6d6..1a22e1fb307f3 100644 --- a/actioncable/lib/action_cable/channel/broadcasting.rb +++ b/actioncable/lib/action_cable/channel/broadcasting.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + require "active_support/core_ext/object/to_param" module ActionCable @@ -7,34 +9,41 @@ module Channel module Broadcasting extend ActiveSupport::Concern - delegate :broadcasting_for, :broadcast_to, to: :class - module ClassMethods - # Broadcast a hash to a unique broadcasting for this model in this channel. + # Broadcast a hash to a unique broadcasting for this `model` in this channel. def broadcast_to(model, message) ActionCable.server.broadcast(broadcasting_for(model), message) end - # Returns a unique broadcasting identifier for this model in this channel: + # Returns a unique broadcasting identifier for this `model` in this channel: # - # CommentsChannel.broadcasting_for("all") # => "comments:all" + # CommentsChannel.broadcasting_for("all") # => "comments:all" # - # You can pass any object as a target (e.g. Active Record model), and it - # would be serialized into a string under the hood. + # You can pass any object as a target (e.g. Active Record model), and it would + # be serialized into a string under the hood. def broadcasting_for(model) serialize_broadcasting([ channel_name, model ]) end - def serialize_broadcasting(object) #:nodoc: - case - when object.is_a?(Array) - object.map { |m| serialize_broadcasting(m) }.join(":") - when object.respond_to?(:to_gid_param) - object.to_gid_param - else - object.to_param + private + def serialize_broadcasting(object) # :nodoc: + case + when object.is_a?(Array) + object.map { |m| serialize_broadcasting(m) }.join(":") + when object.respond_to?(:to_gid_param) + object.to_gid_param + else + object.to_param + end end - end + end + + def broadcasting_for(model) + self.class.broadcasting_for(model) + end + + def broadcast_to(model, message) + self.class.broadcast_to(model, message) end end end diff --git a/actioncable/lib/action_cable/channel/callbacks.rb b/actioncable/lib/action_cable/channel/callbacks.rb index e4cb19b26ac23..5df7bc16943f5 100644 --- a/actioncable/lib/action_cable/channel/callbacks.rb +++ b/actioncable/lib/action_cable/channel/callbacks.rb @@ -1,9 +1,40 @@ # frozen_string_literal: true +# :markup: markdown + require "active_support/callbacks" module ActionCable module Channel + # # Action Cable Channel Callbacks + # + # Action Cable Channel provides callback hooks that are invoked during the life + # cycle of a channel: + # + # * [before_subscribe](rdoc-ref:ClassMethods#before_subscribe) + # * [after_subscribe](rdoc-ref:ClassMethods#after_subscribe) (aliased as + # [on_subscribe](rdoc-ref:ClassMethods#on_subscribe)) + # * [before_unsubscribe](rdoc-ref:ClassMethods#before_unsubscribe) + # * [after_unsubscribe](rdoc-ref:ClassMethods#after_unsubscribe) (aliased as + # [on_unsubscribe](rdoc-ref:ClassMethods#on_unsubscribe)) + # + # + # #### Example + # + # class ChatChannel < ApplicationCable::Channel + # after_subscribe :send_welcome_message, unless: :subscription_rejected? + # after_subscribe :track_subscription + # + # private + # def send_welcome_message + # broadcast_to(...) + # end + # + # def track_subscription + # # ... + # end + # end + # module Callbacks extend ActiveSupport::Concern include ActiveSupport::Callbacks @@ -18,6 +49,14 @@ def before_subscribe(*methods, &block) set_callback(:subscribe, :before, *methods, &block) end + # This callback will be triggered after the Base#subscribed method is called, + # even if the subscription was rejected with the Base#reject method. + # + # To trigger the callback only on successful subscriptions, use the + # Base#subscription_rejected? method: + # + # after_subscribe :my_method, unless: :subscription_rejected? + # def after_subscribe(*methods, &block) set_callback(:subscribe, :after, *methods, &block) end diff --git a/actioncable/lib/action_cable/channel/naming.rb b/actioncable/lib/action_cable/channel/naming.rb index 9c324a2a53275..9a17fc514bd2a 100644 --- a/actioncable/lib/action_cable/channel/naming.rb +++ b/actioncable/lib/action_cable/channel/naming.rb @@ -1,25 +1,28 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module Channel module Naming extend ActiveSupport::Concern module ClassMethods - # Returns the name of the channel, underscored, without the Channel ending. - # If the channel is in a namespace, then the namespaces are represented by single + # Returns the name of the channel, underscored, without the `Channel` ending. If + # the channel is in a namespace, then the namespaces are represented by single # colon separators in the channel name. # - # ChatChannel.channel_name # => 'chat' - # Chats::AppearancesChannel.channel_name # => 'chats:appearances' - # FooChats::BarAppearancesChannel.channel_name # => 'foo_chats:bar_appearances' + # ChatChannel.channel_name # => 'chat' + # Chats::AppearancesChannel.channel_name # => 'chats:appearances' + # FooChats::BarAppearancesChannel.channel_name # => 'foo_chats:bar_appearances' def channel_name - @channel_name ||= name.sub(/Channel$/, "").gsub("::", ":").underscore + @channel_name ||= name.delete_suffix("Channel").gsub("::", ":").underscore end end - # Delegates to the class' channel_name - delegate :channel_name, to: :class + def channel_name + self.class.channel_name + end end end end diff --git a/actioncable/lib/action_cable/channel/periodic_timers.rb b/actioncable/lib/action_cable/channel/periodic_timers.rb index 830b3efa3c82a..2c5a574626547 100644 --- a/actioncable/lib/action_cable/channel/periodic_timers.rb +++ b/actioncable/lib/action_cable/channel/periodic_timers.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module Channel module PeriodicTimers @@ -13,14 +15,12 @@ module PeriodicTimers end module ClassMethods - # Periodically performs a task on the channel, like updating an online - # user counter, polling a backend for new status messages, sending - # regular "heartbeat" messages, or doing some internal work and giving - # progress updates. + # Periodically performs a task on the channel, like updating an online user + # counter, polling a backend for new status messages, sending regular + # "heartbeat" messages, or doing some internal work and giving progress updates. # - # Pass a method name or lambda argument or provide a block to call. - # Specify the calling period in seconds using the every: - # keyword argument. + # Pass a method name or lambda argument or provide a block to call. Specify the + # calling period in seconds using the `every:` keyword argument. # # periodically :transmit_progress, every: 5.seconds # diff --git a/actioncable/lib/action_cable/channel/streams.rb b/actioncable/lib/action_cable/channel/streams.rb index 25502cf2ab457..bb909b538c116 100644 --- a/actioncable/lib/action_cable/channel/streams.rb +++ b/actioncable/lib/action_cable/channel/streams.rb @@ -1,65 +1,77 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module Channel - # Streams allow channels to route broadcastings to the subscriber. A broadcasting is, as discussed elsewhere, a pubsub queue where any data - # placed into it is automatically sent to the clients that are connected at that time. It's purely an online queue, though. If you're not - # streaming a broadcasting at the very moment it sends out an update, you will not get that update, even if you connect after it has been sent. - # - # Most commonly, the streamed broadcast is sent straight to the subscriber on the client-side. The channel just acts as a connector between - # the two parties (the broadcaster and the channel subscriber). Here's an example of a channel that allows subscribers to get all new - # comments on a given page: - # - # class CommentsChannel < ApplicationCable::Channel - # def follow(data) - # stream_from "comments_for_#{data['recording_id']}" - # end + # # Action Cable Channel Streams + # + # Streams allow channels to route broadcastings to the subscriber. A + # broadcasting is, as discussed elsewhere, a pubsub queue where any data placed + # into it is automatically sent to the clients that are connected at that time. + # It's purely an online queue, though. If you're not streaming a broadcasting at + # the very moment it sends out an update, you will not get that update, even if + # you connect after it has been sent. + # + # Most commonly, the streamed broadcast is sent straight to the subscriber on + # the client-side. The channel just acts as a connector between the two parties + # (the broadcaster and the channel subscriber). Here's an example of a channel + # that allows subscribers to get all new comments on a given page: + # + # class CommentsChannel < ApplicationCable::Channel + # def follow(data) + # stream_from "comments_for_#{data['recording_id']}" + # end # - # def unfollow - # stop_all_streams + # def unfollow + # stop_all_streams + # end # end - # end # - # Based on the above example, the subscribers of this channel will get whatever data is put into the, - # let's say, comments_for_45 broadcasting as soon as it's put there. + # Based on the above example, the subscribers of this channel will get whatever + # data is put into the, let's say, `comments_for_45` broadcasting as soon as + # it's put there. # # An example broadcasting for this channel looks like so: # - # ActionCable.server.broadcast "comments_for_45", author: 'DHH', content: 'Rails is just swell' + # ActionCable.server.broadcast "comments_for_45", { author: 'DHH', content: 'Rails is just swell' } # - # If you have a stream that is related to a model, then the broadcasting used can be generated from the model and channel. - # The following example would subscribe to a broadcasting like comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE. + # If you have a stream that is related to a model, then the broadcasting used + # can be generated from the model and channel. The following example would + # subscribe to a broadcasting like `comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE`. # - # class CommentsChannel < ApplicationCable::Channel - # def subscribed - # post = Post.find(params[:id]) - # stream_for post + # class CommentsChannel < ApplicationCable::Channel + # def subscribed + # post = Post.find(params[:id]) + # stream_for post + # end # end - # end # # You can then broadcast to this channel using: # - # CommentsChannel.broadcast_to(@post, @comment) + # CommentsChannel.broadcast_to(@post, @comment) # - # If you don't just want to parlay the broadcast unfiltered to the subscriber, you can also supply a callback that lets you alter what is sent out. - # The below example shows how you can use this to provide performance introspection in the process: + # If you don't just want to parlay the broadcast unfiltered to the subscriber, + # you can also supply a callback that lets you alter what is sent out. The below + # example shows how you can use this to provide performance introspection in the + # process: # - # class ChatChannel < ApplicationCable::Channel - # def subscribed - # @room = Chat::Room[params[:room_number]] + # class ChatChannel < ApplicationCable::Channel + # def subscribed + # @room = Chat::Room[params[:room_number]] # - # stream_for @room, coder: ActiveSupport::JSON do |message| - # if message['originated_at'].present? - # elapsed_time = (Time.now.to_f - message['originated_at']).round(2) + # stream_for @room, coder: ActiveSupport::JSON do |message| + # if message['originated_at'].present? + # elapsed_time = (Time.now.to_f - message['originated_at']).round(2) # - # ActiveSupport::Notifications.instrument :performance, measurement: 'Chat.message_delay', value: elapsed_time, action: :timing - # logger.info "Message took #{elapsed_time}s to arrive" - # end + # ActiveSupport::Notifications.instrument :performance, measurement: 'Chat.message_delay', value: elapsed_time, action: :timing + # logger.info "Message took #{elapsed_time}s to arrive" + # end # - # transmit message + # transmit message + # end # end # end - # end # # You can stop streaming from all broadcasts by calling #stop_all_streams. module Streams @@ -69,18 +81,22 @@ module Streams on_unsubscribe :stop_all_streams end - # Start streaming from the named broadcasting pubsub queue. Optionally, you can pass a callback that'll be used - # instead of the default of just transmitting the updates straight to the subscriber. - # Pass coder: ActiveSupport::JSON to decode messages as JSON before passing to the callback. - # Defaults to coder: nil which does no decoding, passes raw messages. + # Start streaming from the named `broadcasting` pubsub queue. Optionally, you + # can pass a `callback` that'll be used instead of the default of just + # transmitting the updates straight to the subscriber. Pass `coder: + # ActiveSupport::JSON` to decode messages as JSON before passing to the + # callback. Defaults to `coder: nil` which does no decoding, passes raw + # messages. def stream_from(broadcasting, callback = nil, coder: nil, &block) + return if unsubscribed? + broadcasting = String(broadcasting) # Don't send the confirmation until pubsub#subscribe is successful defer_subscription_confirmation! - # Build a stream handler by wrapping the user-provided callback with - # a decoder or defaulting to a JSON-decoding retransmitter. + # Build a stream handler by wrapping the user-provided callback with a decoder + # or defaulting to a JSON-decoding retransmitter. handler = worker_pool_stream_handler(broadcasting, callback || block, coder: coder) streams[broadcasting] = handler @@ -92,17 +108,18 @@ def stream_from(broadcasting, callback = nil, coder: nil, &block) end end - # Start streaming the pubsub queue for the model in this channel. Optionally, you can pass a - # callback that'll be used instead of the default of just transmitting the updates straight - # to the subscriber. + # Start streaming the pubsub queue for the `model` in this channel. Optionally, + # you can pass a `callback` that'll be used instead of the default of just + # transmitting the updates straight to the subscriber. # - # Pass coder: ActiveSupport::JSON to decode messages as JSON before passing to the callback. - # Defaults to coder: nil which does no decoding, passes raw messages. + # Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to + # the callback. Defaults to `coder: nil` which does no decoding, passes raw + # messages. def stream_for(model, callback = nil, coder: nil, &block) stream_from(broadcasting_for(model), callback || block, coder: coder) end - # Unsubscribes streams from the named broadcasting. + # Unsubscribes streams from the named `broadcasting`. def stop_stream_from(broadcasting) callback = streams.delete(broadcasting) if callback @@ -111,7 +128,7 @@ def stop_stream_from(broadcasting) end end - # Unsubscribes streams for the model. + # Unsubscribes streams for the `model`. def stop_stream_for(model) stop_stream_from(broadcasting_for(model)) end @@ -124,13 +141,11 @@ def stop_all_streams end.clear end - # Calls stream_for if record is present, otherwise calls reject. - # This method is intended to be called when you're looking - # for a record based on a parameter, if its found it will start - # streaming. If the record is nil then it will reject the connection. - def stream_or_reject_for(record) - if record - stream_for record + # Calls stream_for with the given `model` if it's present to start streaming, + # otherwise rejects the subscription. + def stream_or_reject_for(model) + if model + stream_for model else reject end @@ -143,8 +158,8 @@ def streams @_streams ||= {} end - # Always wrap the outermost handler to invoke the user handler on the - # worker pool rather than blocking the event loop. + # Always wrap the outermost handler to invoke the user handler on the worker + # pool rather than blocking the event loop. def worker_pool_stream_handler(broadcasting, user_handler, coder: nil) handler = stream_handler(broadcasting, user_handler, coder: coder) @@ -153,8 +168,8 @@ def worker_pool_stream_handler(broadcasting, user_handler, coder: nil) end end - # May be overridden to add instrumentation, logging, specialized error - # handling, or other forms of handler decoration. + # May be overridden to add instrumentation, logging, specialized error handling, + # or other forms of handler decoration. # # TODO: Tests demonstrating this. def stream_handler(broadcasting, user_handler, coder: nil) @@ -165,14 +180,14 @@ def stream_handler(broadcasting, user_handler, coder: nil) end end - # May be overridden to change the default stream handling behavior - # which decodes JSON and transmits to the client. + # May be overridden to change the default stream handling behavior which decodes + # JSON and transmits to the client. # # TODO: Tests demonstrating this. # - # TODO: Room for optimization. Update transmit API to be coder-aware - # so we can no-op when pubsub and connection are both JSON-encoded. - # Then we can skip decode+encode if we're just proxying messages. + # TODO: Room for optimization. Update transmit API to be coder-aware so we can + # no-op when pubsub and connection are both JSON-encoded. Then we can skip + # decode+encode if we're just proxying messages. def default_stream_handler(broadcasting, coder:) coder ||= ActiveSupport::JSON stream_transmitter stream_decoder(coder: coder), broadcasting: broadcasting diff --git a/actioncable/lib/action_cable/channel/test_case.rb b/actioncable/lib/action_cable/channel/test_case.rb index a140d5db91157..d3b38ca5151bb 100644 --- a/actioncable/lib/action_cable/channel/test_case.rb +++ b/actioncable/lib/action_cable/channel/test_case.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + require "active_support" require "active_support/test_case" require "active_support/core_ext/hash/indifferent_access" @@ -15,9 +17,10 @@ def initialize(name) end end - # Stub `stream_from` to track streams for the channel. - # Add public aliases for `subscription_confirmation_sent?` and - # `subscription_rejected?`. + # # Action Cable Channel Stub + # + # Stub `stream_from` to track streams for the channel. Add public aliases for + # `subscription_confirmation_sent?` and `subscription_rejected?`. module ChannelStub def confirmed? subscription_confirmation_sent? @@ -45,9 +48,12 @@ def start_periodic_timers; end end class ConnectionStub - attr_reader :transmissions, :identifiers, :subscriptions, :logger + attr_reader :server, :transmissions, :identifiers, :subscriptions, :logger + + delegate :pubsub, :config, to: :server def initialize(identifiers = {}) + @server = ActionCable.server @transmissions = [] identifiers.each do |identifier, val| @@ -62,107 +68,125 @@ def initialize(identifiers = {}) def transmit(cable_message) transmissions << cable_message.with_indifferent_access end + + def connection_identifier + @connection_identifier ||= connection_gid(identifiers.filter_map { |id| send(id.to_sym) if id }) + end + + private + def connection_gid(ids) + ids.map do |o| + if o.respond_to?(:to_gid_param) + o.to_gid_param + else + o.to_s + end + end.sort.join(":") + end end # Superclass for Action Cable channel functional tests. # - # == Basic example + # ## Basic example # # Functional tests are written as follows: - # 1. First, one uses the +subscribe+ method to simulate subscription creation. - # 2. Then, one asserts whether the current state is as expected. "State" can be anything: - # transmitted messages, subscribed streams, etc. + # 1. First, one uses the `subscribe` method to simulate subscription creation. + # 2. Then, one asserts whether the current state is as expected. "State" can be + # anything: transmitted messages, subscribed streams, etc. + # # # For example: # - # class ChatChannelTest < ActionCable::Channel::TestCase - # def test_subscribed_with_room_number - # # Simulate a subscription creation - # subscribe room_number: 1 + # class ChatChannelTest < ActionCable::Channel::TestCase + # def test_subscribed_with_room_number + # # Simulate a subscription creation + # subscribe room_number: 1 # - # # Asserts that the subscription was successfully created - # assert subscription.confirmed? + # # Asserts that the subscription was successfully created + # assert subscription.confirmed? # - # # Asserts that the channel subscribes connection to a stream - # assert_has_stream "chat_1" + # # Asserts that the channel subscribes connection to a stream + # assert_has_stream "chat_1" # - # # Asserts that the channel subscribes connection to a specific - # # stream created for a model - # assert_has_stream_for Room.find(1) - # end + # # Asserts that the channel subscribes connection to a specific + # # stream created for a model + # assert_has_stream_for Room.find(1) + # end # - # def test_does_not_stream_with_incorrect_room_number - # subscribe room_number: -1 + # def test_does_not_stream_with_incorrect_room_number + # subscribe room_number: -1 # - # # Asserts that not streams was started - # assert_no_streams - # end + # # Asserts that not streams was started + # assert_no_streams + # end # - # def test_does_not_subscribe_without_room_number - # subscribe + # def test_does_not_subscribe_without_room_number + # subscribe # - # # Asserts that the subscription was rejected - # assert subscription.rejected? + # # Asserts that the subscription was rejected + # assert subscription.rejected? + # end # end - # end # # You can also perform actions: - # def test_perform_speak - # subscribe room_number: 1 + # def test_perform_speak + # subscribe room_number: 1 # - # perform :speak, message: "Hello, Rails!" + # perform :speak, message: "Hello, Rails!" # - # assert_equal "Hello, Rails!", transmissions.last["text"] - # end + # assert_equal "Hello, Rails!", transmissions.last["text"] + # end # - # == Special methods + # ## Special methods # - # ActionCable::Channel::TestCase will also automatically provide the following instance - # methods for use in the tests: + # ActionCable::Channel::TestCase will also automatically provide the following + # instance methods for use in the tests: # - # connection:: - # An ActionCable::Channel::ConnectionStub, representing the current HTTP connection. - # subscription:: - # An instance of the current channel, created when you call `subscribe`. - # transmissions:: - # A list of all messages that have been transmitted into the channel. + # connection + # : An ActionCable::Channel::ConnectionStub, representing the current HTTP + # connection. # + # subscription + # : An instance of the current channel, created when you call `subscribe`. # - # == Channel is automatically inferred + # transmissions + # : A list of all messages that have been transmitted into the channel. + # + # + # ## Channel is automatically inferred # # ActionCable::Channel::TestCase will automatically infer the channel under test # from the test class name. If the channel cannot be inferred from the test - # class name, you can explicitly set it with +tests+. + # class name, you can explicitly set it with `tests`. # - # class SpecialEdgeCaseChannelTest < ActionCable::Channel::TestCase - # tests SpecialChannel - # end - # - # == Specifying connection identifiers + # class SpecialEdgeCaseChannelTest < ActionCable::Channel::TestCase + # tests SpecialChannel + # end # - # You need to set up your connection manually to provide values for the identifiers. - # To do this just use: + # ## Specifying connection identifiers # - # stub_connection(user: users(:john)) + # You need to set up your connection manually to provide values for the + # identifiers. To do this just use: # - # == Testing broadcasting + # stub_connection(user: users(:john)) # - # ActionCable::Channel::TestCase enhances ActionCable::TestHelper assertions (e.g. - # +assert_broadcasts+) to handle broadcasting to models: + # ## Testing broadcasting # + # ActionCable::Channel::TestCase enhances ActionCable::TestHelper assertions + # (e.g. `assert_broadcasts`) to handle broadcasting to models: # - # # in your channel - # def speak(data) - # broadcast_to room, text: data["message"] - # end + # # in your channel + # def speak(data) + # broadcast_to room, text: data["message"] + # end # - # def test_speak - # subscribe room_id: rooms(:chat).id + # def test_speak + # subscribe room_id: rooms(:chat).id # - # assert_broadcast_on(rooms(:chat), text: "Hello, Rails!") do - # perform :speak, message: "Hello, Rails!" - # end - # end + # assert_broadcast_on(rooms(:chat), text: "Hello, Rails!") do + # perform :speak, message: "Hello, Rails!" + # end + # end class TestCase < ActiveSupport::TestCase module Behavior extend ActiveSupport::Concern @@ -211,16 +235,17 @@ def determine_default_channel(name) # Set up test connection with the specified identifiers: # - # class ApplicationCable < ActionCable::Connection::Base - # identified_by :user, :token - # end + # class ApplicationCable < ActionCable::Connection::Base + # identified_by :user, :token + # end # - # stub_connection(user: users[:john], token: 'my-secret-token') + # stub_connection(user: users[:john], token: 'my-secret-token') def stub_connection(identifiers = {}) @connection = ConnectionStub.new(identifiers) end - # Subscribe to the channel under test. Optionally pass subscription parameters as a Hash. + # Subscribe to the channel under test. Optionally pass subscription parameters + # as a Hash. def subscribe(params = {}) @connection ||= stub_connection @subscription = self.class.channel_class.new(connection, CHANNEL_IDENTIFIER, params.with_indifferent_access) @@ -246,11 +271,10 @@ def perform(action, data = {}) # Returns messages transmitted into channel def transmissions # Return only directly sent message (via #transmit) - connection.transmissions.map { |data| data["message"] }.compact + connection.transmissions.filter_map { |data| data["message"] } end - # Enhance TestHelper assertions to handle non-String - # broadcastings + # Enhance TestHelper assertions to handle non-String broadcastings def assert_broadcasts(stream_or_object, *args) super(broadcasting_for(stream_or_object), *args) end @@ -261,10 +285,10 @@ def assert_broadcast_on(stream_or_object, *args) # Asserts that no streams have been started. # - # def test_assert_no_started_stream - # subscribe - # assert_no_streams - # end + # def test_assert_no_started_stream + # subscribe + # assert_no_streams + # end # def assert_no_streams assert subscription.streams.empty?, "No streams started was expected, but #{subscription.streams.count} found" @@ -272,10 +296,10 @@ def assert_no_streams # Asserts that the specified stream has been started. # - # def test_assert_started_stream - # subscribe - # assert_has_stream 'messages' - # end + # def test_assert_started_stream + # subscribe + # assert_has_stream 'messages' + # end # def assert_has_stream(stream) assert subscription.streams.include?(stream), "Stream #{stream} has not been started" @@ -283,15 +307,37 @@ def assert_has_stream(stream) # Asserts that the specified stream for a model has started. # - # def test_assert_started_stream_for - # subscribe id: 42 - # assert_has_stream_for User.find(42) - # end + # def test_assert_started_stream_for + # subscribe id: 42 + # assert_has_stream_for User.find(42) + # end # def assert_has_stream_for(object) assert_has_stream(broadcasting_for(object)) end + # Asserts that the specified stream has not been started. + # + # def test_assert_no_started_stream + # subscribe + # assert_has_no_stream 'messages' + # end + # + def assert_has_no_stream(stream) + assert subscription.streams.exclude?(stream), "Stream #{stream} has been started" + end + + # Asserts that the specified stream for a model has not started. + # + # def test_assert_no_started_stream_for + # subscribe id: 41 + # assert_has_no_stream_for User.find(42) + # end + # + def assert_has_no_stream_for(object) + assert_has_no_stream(broadcasting_for(object)) + end + private def check_subscribed! raise "Must be subscribed!" if subscription.nil? || subscription.rejected? diff --git a/actioncable/lib/action_cable/connection.rb b/actioncable/lib/action_cable/connection.rb deleted file mode 100644 index 20b5dbe78d9f6..0000000000000 --- a/actioncable/lib/action_cable/connection.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module ActionCable - module Connection - extend ActiveSupport::Autoload - - eager_autoload do - autoload :Authorization - autoload :Base - autoload :ClientSocket - autoload :Identification - autoload :InternalChannel - autoload :MessageBuffer - autoload :Stream - autoload :StreamEventLoop - autoload :Subscriptions - autoload :TaggedLoggerProxy - autoload :TestCase - autoload :WebSocket - end - end -end diff --git a/actioncable/lib/action_cable/connection/authorization.rb b/actioncable/lib/action_cable/connection/authorization.rb index aef3386f71aeb..de996e30517db 100644 --- a/actioncable/lib/action_cable/connection/authorization.rb +++ b/actioncable/lib/action_cable/connection/authorization.rb @@ -1,11 +1,14 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module Connection module Authorization class UnauthorizedError < StandardError; end - # Closes the WebSocket connection if it is open and returns a 404 "File not Found" response. + # Closes the WebSocket connection if it is open and returns an "unauthorized" + # reason. def reject_unauthorized_connection logger.error "An unauthorized connection attempt was rejected" raise UnauthorizedError diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb index a243a91def60b..5ac3bc646b18f 100644 --- a/actioncable/lib/action_cable/connection/base.rb +++ b/actioncable/lib/action_cable/connection/base.rb @@ -1,56 +1,68 @@ # frozen_string_literal: true +# :markup: markdown + require "action_dispatch" require "active_support/rescuable" module ActionCable module Connection - # For every WebSocket connection the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent - # of all of the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions - # based on an identifier sent by the Action Cable consumer. The Connection itself does not deal with any specific application logic beyond - # authentication and authorization. + # # Action Cable Connection Base # - # Here's a basic example: + # For every WebSocket connection the Action Cable server accepts, a Connection + # object will be instantiated. This instance becomes the parent of all of the + # channel subscriptions that are created from there on. Incoming messages are + # then routed to these channel subscriptions based on an identifier sent by the + # Action Cable consumer. The Connection itself does not deal with any specific + # application logic beyond authentication and authorization. # - # module ApplicationCable - # class Connection < ActionCable::Connection::Base - # identified_by :current_user + # Here's a basic example: # - # def connect - # self.current_user = find_verified_user - # logger.add_tags current_user.name - # end + # module ApplicationCable + # class Connection < ActionCable::Connection::Base + # identified_by :current_user # - # def disconnect - # # Any cleanup work needed when the cable connection is cut. - # end + # def connect + # self.current_user = find_verified_user + # logger.add_tags current_user.name + # end # - # private - # def find_verified_user - # User.find_by_identity(cookies.encrypted[:identity_id]) || - # reject_unauthorized_connection + # def disconnect + # # Any cleanup work needed when the cable connection is cut. # end + # + # private + # def find_verified_user + # User.find_by_identity(cookies.encrypted[:identity_id]) || + # reject_unauthorized_connection + # end + # end # end - # end # - # First, we declare that this connection can be identified by its current_user. This allows us to later be able to find all connections - # established for that current_user (and potentially disconnect them). You can declare as many - # identification indexes as you like. Declaring an identification means that an attr_accessor is automatically set for that key. + # First, we declare that this connection can be identified by its current_user. + # This allows us to later be able to find all connections established for that + # current_user (and potentially disconnect them). You can declare as many + # identification indexes as you like. Declaring an identification means that an + # attr_accessor is automatically set for that key. # - # Second, we rely on the fact that the WebSocket connection is established with the cookies from the domain being sent along. This makes - # it easy to use signed cookies that were set when logging in via a web interface to authorize the WebSocket connection. + # Second, we rely on the fact that the WebSocket connection is established with + # the cookies from the domain being sent along. This makes it easy to use signed + # cookies that were set when logging in via a web interface to authorize the + # WebSocket connection. # - # Finally, we add a tag to the connection-specific logger with the name of the current user to easily distinguish their messages in the log. + # Finally, we add a tag to the connection-specific logger with the name of the + # current user to easily distinguish their messages in the log. # # Pretty simple, eh? class Base include Identification include InternalChannel include Authorization + include Callbacks include ActiveSupport::Rescuable attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol - delegate :event_loop, :pubsub, to: :server + delegate :event_loop, :pubsub, :config, to: :server def initialize(server, env, coder: ActiveSupport::JSON) @server, @env, @coder = server, env, coder @@ -66,9 +78,11 @@ def initialize(server, env, coder: ActiveSupport::JSON) @started_at = Time.now end - # Called by the server when a new WebSocket connection is established. This configures the callbacks intended for overwriting by the user. - # This method should not be called directly -- instead rely upon on the #connect (and #disconnect) callbacks. - def process #:nodoc: + # Called by the server when a new WebSocket connection is established. This + # configures the callbacks intended for overwriting by the user. This method + # should not be called directly -- instead rely upon on the #connect (and + # #disconnect) callbacks. + def process # :nodoc: logger.info started_request_message if websocket.possible? && allow_request_origin? @@ -80,18 +94,24 @@ def process #:nodoc: # Decodes WebSocket messages and dispatches them to subscribed channels. # WebSocket message transfer encoding is always JSON. - def receive(websocket_message) #:nodoc: + def receive(websocket_message) # :nodoc: send_async :dispatch_websocket_message, websocket_message end - def dispatch_websocket_message(websocket_message) #:nodoc: + def dispatch_websocket_message(websocket_message) # :nodoc: if websocket.alive? - subscriptions.execute_command decode(websocket_message) + handle_channel_command decode(websocket_message) else logger.error "Ignoring message processed after the WebSocket was closed: #{websocket_message.inspect})" end end + def handle_channel_command(payload) + run_callbacks :command do + subscriptions.execute_command payload + end + end + def transmit(cable_message) # :nodoc: websocket.transmit encode(cable_message) end @@ -106,13 +126,15 @@ def close(reason: nil, reconnect: true) websocket.close end - # Invoke a method on the connection asynchronously through the pool of thread workers. + # Invoke a method on the connection asynchronously through the pool of thread + # workers. def send_async(method, *arguments) worker_pool.async_invoke(self, method, *arguments) end - # Return a basic hash of statistics for the connection keyed with identifier, started_at, subscriptions, and request_id. - # This can be returned by a health check against the connection. + # Return a basic hash of statistics for the connection keyed with `identifier`, + # `started_at`, `subscriptions`, and `request_id`. This can be returned by a + # health check against the connection. def statistics { identifier: connection_identifier, @@ -143,11 +165,16 @@ def on_close(reason, code) # :nodoc: send_async :handle_close end + def inspect # :nodoc: + "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>" + end + private attr_reader :websocket attr_reader :message_buffer - # The request that initiated the WebSocket connection is available here. This gives access to the environment, cookies, etc. + # The request that initiated the WebSocket connection is available here. This + # gives access to the environment, cookies, etc. def request # :doc: @request ||= begin environment = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application @@ -155,7 +182,8 @@ def request # :doc: end end - # The cookies of the request that initiated the WebSocket connection. Useful for performing authorization checks. + # The cookies of the request that initiated the WebSocket connection. Useful for + # performing authorization checks. def cookies # :doc: request.cookie_jar end @@ -192,9 +220,8 @@ def handle_close end def send_welcome_message - # Send welcome message to the internal connection monitor channel. - # This ensures the connection monitor state is reset after a successful - # websocket connection. + # Send welcome message to the internal connection monitor channel. This ensures + # the connection monitor state is reset after a successful websocket connection. transmit type: ActionCable::INTERNAL[:message_types][:welcome] end @@ -222,10 +249,11 @@ def respond_to_invalid_request logger.error invalid_request_message logger.info finished_request_message - [ 404, { "Content-Type" => "text/plain" }, [ "Page not found" ] ] + [ 404, { Rack::CONTENT_TYPE => "text/plain; charset=utf-8" }, [ "Page not found" ] ] end - # Tags are declared in the server but computed in the connection. This allows us per-connection tailored tags. + # Tags are declared in the server but computed in the connection. This allows us + # per-connection tailored tags. def new_tagged_logger TaggedLoggerProxy.new server.logger, tags: server.config.log_tags.map { |tag| tag.respond_to?(:call) ? tag.call(request) : tag.to_s.camelize } diff --git a/actioncable/lib/action_cable/connection/callbacks.rb b/actioncable/lib/action_cable/connection/callbacks.rb new file mode 100644 index 0000000000000..85a27c6f9f5f6 --- /dev/null +++ b/actioncable/lib/action_cable/connection/callbacks.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# :markup: markdown + +require "active_support/callbacks" + +module ActionCable + module Connection + # # Action Cable Connection Callbacks + # + # The [before_command](rdoc-ref:ClassMethods#before_command), + # [after_command](rdoc-ref:ClassMethods#after_command), and + # [around_command](rdoc-ref:ClassMethods#around_command) callbacks are invoked + # when sending commands to the client, such as when subscribing, unsubscribing, + # or performing an action. + # + # #### Example + # + # module ApplicationCable + # class Connection < ActionCable::Connection::Base + # identified_by :user + # + # around_command :set_current_account + # + # private + # + # def set_current_account + # # Now all channels could use Current.account + # Current.set(account: user.account) { yield } + # end + # end + # end + # + module Callbacks + extend ActiveSupport::Concern + include ActiveSupport::Callbacks + + included do + define_callbacks :command + end + + module ClassMethods + def before_command(*methods, &block) + set_callback(:command, :before, *methods, &block) + end + + def after_command(*methods, &block) + set_callback(:command, :after, *methods, &block) + end + + def around_command(*methods, &block) + set_callback(:command, :around, *methods, &block) + end + end + end + end +end diff --git a/actioncable/lib/action_cable/connection/client_socket.rb b/actioncable/lib/action_cable/connection/client_socket.rb index 4b1964c4ae760..9d8be5e92fe06 100644 --- a/actioncable/lib/action_cable/connection/client_socket.rb +++ b/actioncable/lib/action_cable/connection/client_socket.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + require "websocket/driver" module ActionCable @@ -43,7 +45,7 @@ def initialize(env, event_target, event_loop, protocols) @ready_state = CONNECTING - # The driver calls +env+, +url+, and +write+ + # The driver calls `env`, `url`, and `write` @driver = ::WebSocket::Driver.rack(self, protocols: protocols) @driver.on(:open) { |e| open } diff --git a/actioncable/lib/action_cable/connection/identification.rb b/actioncable/lib/action_cable/connection/identification.rb index cc544685ddc1c..663fba60ac24e 100644 --- a/actioncable/lib/action_cable/connection/identification.rb +++ b/actioncable/lib/action_cable/connection/identification.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "set" +# :markup: markdown module ActionCable module Connection @@ -12,21 +12,23 @@ module Identification end module ClassMethods - # Mark a key as being a connection identifier index that can then be used to find the specific connection again later. - # Common identifiers are current_user and current_account, but could be anything, really. + # Mark a key as being a connection identifier index that can then be used to + # find the specific connection again later. Common identifiers are current_user + # and current_account, but could be anything, really. # - # Note that anything marked as an identifier will automatically create a delegate by the same name on any - # channel instances created off the connection. + # Note that anything marked as an identifier will automatically create a + # delegate by the same name on any channel instances created off the connection. def identified_by(*identifiers) Array(identifiers).each { |identifier| attr_accessor identifier } self.identifiers += identifiers end end - # Return a single connection identifier that combines the value of all the registered identifiers into a single gid. + # Return a single connection identifier that combines the value of all the + # registered identifiers into a single gid. def connection_identifier unless defined? @connection_identifier - @connection_identifier = connection_gid identifiers.map { |id| instance_variable_get("@#{id}") }.compact + @connection_identifier = connection_gid identifiers.filter_map { |id| instance_variable_get("@#{id}") } end @connection_identifier diff --git a/actioncable/lib/action_cable/connection/internal_channel.rb b/actioncable/lib/action_cable/connection/internal_channel.rb index f03904137b6db..23933e8660bea 100644 --- a/actioncable/lib/action_cable/connection/internal_channel.rb +++ b/actioncable/lib/action_cable/connection/internal_channel.rb @@ -1,8 +1,13 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module Connection - # Makes it possible for the RemoteConnection to disconnect a specific connection. + # # Action Cable InternalChannel + # + # Makes it possible for the RemoteConnection to disconnect a specific + # connection. module InternalChannel extend ActiveSupport::Concern @@ -32,7 +37,7 @@ def process_internal_message(message) case message["type"] when "disconnect" logger.info "Removing connection (#{connection_identifier})" - websocket.close + close(reason: ActionCable::INTERNAL[:disconnect_reasons][:remote], reconnect: message.fetch("reconnect", true)) end rescue Exception => e logger.error "There was an exception - #{e.class}(#{e.message})" diff --git a/actioncable/lib/action_cable/connection/message_buffer.rb b/actioncable/lib/action_cable/connection/message_buffer.rb index 965841b67e7da..35813930244a4 100644 --- a/actioncable/lib/action_cable/connection/message_buffer.rb +++ b/actioncable/lib/action_cable/connection/message_buffer.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module Connection - # Allows us to buffer messages received from the WebSocket before the Connection has been fully initialized, and is ready to receive them. + # Allows us to buffer messages received from the WebSocket before the Connection + # has been fully initialized, and is ready to receive them. class MessageBuffer # :nodoc: def initialize(connection) @connection = connection diff --git a/actioncable/lib/action_cable/connection/stream.rb b/actioncable/lib/action_cable/connection/stream.rb index e658948a55c6b..01c2d114ec199 100644 --- a/actioncable/lib/action_cable/connection/stream.rb +++ b/actioncable/lib/action_cable/connection/stream.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "thread" +# :markup: markdown module ActionCable module Connection @@ -100,7 +100,7 @@ def hijack_rack_socket # This should return the underlying io according to the SPEC: @rack_hijack_io = @socket_object.env["rack.hijack"].call - # Retain existing behaviour if required: + # Retain existing behavior if required: @rack_hijack_io ||= @socket_object.env["rack.hijack_io"] @event_loop.attach(@rack_hijack_io, self) diff --git a/actioncable/lib/action_cable/connection/stream_event_loop.rb b/actioncable/lib/action_cable/connection/stream_event_loop.rb index d95afc50bad3f..38e9823b7e06b 100644 --- a/actioncable/lib/action_cable/connection/stream_event_loop.rb +++ b/actioncable/lib/action_cable/connection/stream_event_loop.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true +# :markup: markdown + require "nio" -require "thread" module ActionCable module Connection @@ -67,6 +68,7 @@ def spawn @nio ||= NIO::Selector.new @executor ||= Concurrent::ThreadPoolExecutor.new( + name: "ActionCable-streamer", min_threads: 1, max_threads: 10, max_queue: 0, @@ -117,9 +119,8 @@ def run stream.receive incoming end rescue - # We expect one of EOFError or Errno::ECONNRESET in - # normal operation (when the client goes away). But if - # anything else goes wrong, this is still the best way + # We expect one of EOFError or Errno::ECONNRESET in normal operation (when the + # client goes away). But if anything else goes wrong, this is still the best way # to handle it. begin stream.close diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb index 41b4cc4e922d9..a9b1bca7cbc7a 100644 --- a/actioncable/lib/action_cable/connection/subscriptions.rb +++ b/actioncable/lib/action_cable/connection/subscriptions.rb @@ -1,11 +1,16 @@ # frozen_string_literal: true +# :markup: markdown + require "active_support/core_ext/hash/indifferent_access" module ActionCable module Connection - # Collection class for all the channel subscriptions established on a given connection. Responsible for routing incoming commands that arrive on - # the connection to the proper channel. + # # Action Cable Connection Subscriptions + # + # Collection class for all the channel subscriptions established on a given + # connection. Responsible for routing incoming commands that arrive on the + # connection to the proper channel. class Subscriptions # :nodoc: def initialize(connection) @connection = connection @@ -33,7 +38,7 @@ def add(data) subscription_klass = id_options[:channel].safe_constantize - if subscription_klass && ActionCable::Channel::Base >= subscription_klass + if subscription_klass && ActionCable::Channel::Base > subscription_klass subscription = subscription_klass.new(connection, id_key, id_options) subscriptions[id_key] = subscription subscription.subscribe_to_channel diff --git a/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb b/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb index 85831806a94c8..b7d97afb09eee 100644 --- a/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb +++ b/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb @@ -1,10 +1,15 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module Connection - # Allows the use of per-connection tags against the server logger. This wouldn't work using the traditional - # ActiveSupport::TaggedLogging enhanced Rails.logger, as that logger will reset the tags between requests. - # The connection is long-lived, so it needs its own set of tags for its independent duration. + # # Action Cable Connection TaggedLoggerProxy + # + # Allows the use of per-connection tags against the server logger. This wouldn't + # work using the traditional ActiveSupport::TaggedLogging enhanced Rails.logger, + # as that logger will reset the tags between requests. The connection is + # long-lived, so it needs its own set of tags for its independent duration. class TaggedLoggerProxy attr_reader :tags @@ -18,24 +23,24 @@ def add_tags(*tags) @tags = @tags.uniq end - def tag(logger) + def tag(logger, &block) if logger.respond_to?(:tagged) current_tags = tags - logger.formatter.current_tags - logger.tagged(*current_tags) { yield } + logger.tagged(*current_tags, &block) else yield end end %i( debug info warn error fatal unknown ).each do |severity| - define_method(severity) do |message| - log severity, message + define_method(severity) do |message = nil, &block| + log severity, message, &block end end private - def log(type, message) # :doc: - tag(@logger) { @logger.send type, message } + def log(type, message, &block) # :doc: + tag(@logger) { @logger.send type, message, &block } end end end diff --git a/actioncable/lib/action_cable/connection/test_case.rb b/actioncable/lib/action_cable/connection/test_case.rb index d8907dd255c07..5eeb0775fd7b3 100644 --- a/actioncable/lib/action_cable/connection/test_case.rb +++ b/actioncable/lib/action_cable/connection/test_case.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + require "active_support" require "active_support/test_case" require "active_support/core_ext/hash/indifferent_access" @@ -18,25 +20,33 @@ def initialize(name) end module Assertions - # Asserts that the connection is rejected (via +reject_unauthorized_connection+). + # Asserts that the connection is rejected (via + # `reject_unauthorized_connection`). # - # # Asserts that connection without user_id fails - # assert_reject_connection { connect params: { user_id: '' } } + # # Asserts that connection without user_id fails + # assert_reject_connection { connect params: { user_id: '' } } def assert_reject_connection(&block) assert_raises(Authorization::UnauthorizedError, "Expected to reject connection but no rejection was made", &block) end end - # We don't want to use the whole "encryption stack" for connection - # unit-tests, but we want to make sure that users test against the correct types - # of cookies (i.e. signed or encrypted or plain) - class TestCookieJar < ActiveSupport::HashWithIndifferentAccess + class TestCookies < ActiveSupport::HashWithIndifferentAccess # :nodoc: + def []=(name, options) + value = options.is_a?(Hash) ? options.symbolize_keys[:value] : options + super(name, value) + end + end + + # We don't want to use the whole "encryption stack" for connection unit-tests, + # but we want to make sure that users test against the correct types of cookies + # (i.e. signed or encrypted or plain) + class TestCookieJar < TestCookies def signed - self[:signed] ||= {}.with_indifferent_access + @signed ||= TestCookies.new end def encrypted - self[:encrypted] ||= {}.with_indifferent_access + @encrypted ||= TestCookies.new end end @@ -56,75 +66,74 @@ def initialize(request) end end + # # Action Cable Connection TestCase + # # Unit test Action Cable connections. # - # Useful to check whether a connection's +identified_by+ gets assigned properly + # Useful to check whether a connection's `identified_by` gets assigned properly # and that any improper connection requests are rejected. # - # == Basic example - # - # Unit tests are written as follows: - # - # 1. Simulate a connection attempt by calling +connect+. - # 2. Assert state, e.g. identifiers, has been assigned. + # ## Basic example # + # Unit tests are written by first simulating a connection attempt by calling + # `connect` and then asserting state, e.g. identifiers, have been assigned. # - # class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase - # def test_connects_with_proper_cookie - # # Simulate the connection request with a cookie. - # cookies["user_id"] = users(:john).id + # class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase + # def test_connects_with_proper_cookie + # # Simulate the connection request with a cookie. + # cookies["user_id"] = users(:john).id # - # connect + # connect # - # # Assert the connection identifier matches the fixture. - # assert_equal users(:john).id, connection.user.id - # end + # # Assert the connection identifier matches the fixture. + # assert_equal users(:john).id, connection.user.id + # end # - # def test_rejects_connection_without_proper_cookie - # assert_reject_connection { connect } + # def test_rejects_connection_without_proper_cookie + # assert_reject_connection { connect } + # end # end - # end # - # +connect+ accepts additional information about the HTTP request with the - # +params+, +headers+, +session+ and Rack +env+ options. + # `connect` accepts additional information about the HTTP request with the + # `params`, `headers`, `session`, and Rack `env` options. # - # def test_connect_with_headers_and_query_string - # connect params: { user_id: 1 }, headers: { "X-API-TOKEN" => "secret-my" } + # def test_connect_with_headers_and_query_string + # connect params: { user_id: 1 }, headers: { "X-API-TOKEN" => "secret-my" } # - # assert_equal "1", connection.user.id - # assert_equal "secret-my", connection.token - # end + # assert_equal "1", connection.user.id + # assert_equal "secret-my", connection.token + # end # - # def test_connect_with_params - # connect params: { user_id: 1 } + # def test_connect_with_params + # connect params: { user_id: 1 } # - # assert_equal "1", connection.user.id - # end + # assert_equal "1", connection.user.id + # end # # You can also set up the correct cookies before the connection request: # - # def test_connect_with_cookies - # # Plain cookies: - # cookies["user_id"] = 1 + # def test_connect_with_cookies + # # Plain cookies: + # cookies["user_id"] = 1 # - # # Or signed/encrypted: - # # cookies.signed["user_id"] = 1 - # # cookies.encrypted["user_id"] = 1 + # # Or signed/encrypted: + # # cookies.signed["user_id"] = 1 + # # cookies.encrypted["user_id"] = 1 # - # connect + # connect # - # assert_equal "1", connection.user_id - # end + # assert_equal "1", connection.user_id + # end # - # == Connection is automatically inferred + # ## Connection is automatically inferred # - # ActionCable::Connection::TestCase will automatically infer the connection under test - # from the test class name. If the channel cannot be inferred from the test - # class name, you can explicitly set it with +tests+. + # ActionCable::Connection::TestCase will automatically infer the connection + # under test from the test class name. If the channel cannot be inferred from + # the test class name, you can explicitly set it with `tests`. # - # class ConnectionTest < ActionCable::Connection::TestCase - # tests ApplicationCable::Connection - # end + # class ConnectionTest < ActionCable::Connection::TestCase + # tests ApplicationCable::Connection + # end # class TestCase < ActiveSupport::TestCase module Behavior @@ -176,10 +185,10 @@ def determine_default_connection(name) # # Accepts request path as the first argument and the following request options: # - # - params – URL parameters (Hash) - # - headers – request headers (Hash) - # - session – session data (Hash) - # - env – additional Rack env configuration (Hash) + # * params – URL parameters (Hash) + # * headers – request headers (Hash) + # * session – session data (Hash) + # * env – additional Rack env configuration (Hash) def connect(path = ActionCable.server.config.mount_path, **request_params) path ||= DEFAULT_PATH diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb index 31f29fdd2fe77..662f5fbb159f6 100644 --- a/actioncable/lib/action_cable/connection/web_socket.rb +++ b/actioncable/lib/action_cable/connection/web_socket.rb @@ -1,9 +1,13 @@ # frozen_string_literal: true +# :markup: markdown + require "websocket/driver" module ActionCable module Connection + # # Action Cable Connection WebSocket + # # Wrap the real socket to minimize the externally-presented API class WebSocket # :nodoc: def initialize(env, event_target, event_loop, protocols: ActionCable::INTERNAL[:protocols]) @@ -15,23 +19,23 @@ def possible? end def alive? - websocket && websocket.alive? + websocket&.alive? end - def transmit(data) - websocket.transmit data + def transmit(...) + websocket&.transmit(...) end - def close - websocket.close + def close(...) + websocket&.close(...) end def protocol - websocket.protocol + websocket&.protocol end def rack_response - websocket.rack_response + websocket&.rack_response end private diff --git a/actioncable/lib/action_cable/deprecator.rb b/actioncable/lib/action_cable/deprecator.rb new file mode 100644 index 0000000000000..b2e74e8ee8e83 --- /dev/null +++ b/actioncable/lib/action_cable/deprecator.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# :markup: markdown + +module ActionCable + def self.deprecator # :nodoc: + @deprecator ||= ActiveSupport::Deprecation.new + end +end diff --git a/actioncable/lib/action_cable/engine.rb b/actioncable/lib/action_cable/engine.rb index 53cbb597cd12c..d8a92d28b650f 100644 --- a/actioncable/lib/action_cable/engine.rb +++ b/actioncable/lib/action_cable/engine.rb @@ -1,16 +1,20 @@ # frozen_string_literal: true +# :markup: markdown + require "rails" require "action_cable" -require "action_cable/helpers/action_cable_helper" require "active_support/core_ext/hash/indifferent_access" module ActionCable class Engine < Rails::Engine # :nodoc: config.action_cable = ActiveSupport::OrderedOptions.new config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path] + config.action_cable.precompile_assets = true - config.eager_load_namespaces << ActionCable + initializer "action_cable.deprecator", before: :load_environment_config do |app| + app.deprecators[:action_cable] = ActionCable.deprecator + end initializer "action_cable.helpers" do ActiveSupport.on_load(:action_view) do @@ -22,6 +26,20 @@ class Engine < Rails::Engine # :nodoc: ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger } end + initializer "action_cable.health_check_application" do + ActiveSupport.on_load(:action_cable) { + self.health_check_application = ->(env) { Rails::HealthController.action(:show).call(env) } + } + end + + initializer "action_cable.asset" do + config.after_initialize do |app| + if app.config.respond_to?(:assets) && app.config.action_cable.precompile_assets + app.config.assets.precompile += %w( actioncable.js actioncable.esm.js ) + end + end + end + initializer "action_cable.set_configs" do |app| options = app.config.action_cable options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development? @@ -30,11 +48,12 @@ class Engine < Rails::Engine # :nodoc: ActiveSupport.on_load(:action_cable) do if (config_path = Pathname.new(app.config.paths["config/cable"].first)).exist? - self.cable = Rails.application.config_for(config_path).with_indifferent_access + self.cable = app.config_for(config_path).to_h.with_indifferent_access end previous_connection_class = connection_class self.connection_class = -> { "ApplicationCable::Connection".safe_constantize || previous_connection_class.call } + self.filter_parameters += app.config.filter_parameters options.each { |k, v| send("#{k}=", v) } end @@ -45,7 +64,7 @@ class Engine < Rails::Engine # :nodoc: config = app.config unless config.action_cable.mount_path.nil? app.routes.prepend do - mount ActionCable.server => config.action_cable.mount_path, internal: true + mount ActionCable.server => config.action_cable.mount_path, internal: true, anchor: true end end end @@ -54,10 +73,10 @@ class Engine < Rails::Engine # :nodoc: initializer "action_cable.set_work_hooks" do |app| ActiveSupport.on_load(:action_cable) do ActionCable::Server::Worker.set_callback :work, :around, prepend: true do |_, inner| - app.executor.wrap do - # If we took a while to get the lock, we may have been halted - # in the meantime. As we haven't started doing any real work - # yet, we should pretend that we never made it off the queue. + app.executor.wrap(source: "application.action_cable") do + # If we took a while to get the lock, we may have been halted in the meantime. + # As we haven't started doing any real work yet, we should pretend that we never + # made it off the queue. unless stopping? inner.call end @@ -65,7 +84,7 @@ class Engine < Rails::Engine # :nodoc: end wrap = lambda do |_, inner| - app.executor.wrap(&inner) + app.executor.wrap(source: "application.action_cable", &inner) end ActionCable::Channel::Base.set_callback :subscribe, :around, prepend: true, &wrap ActionCable::Channel::Base.set_callback :unsubscribe, :around, prepend: true, &wrap diff --git a/actioncable/lib/action_cable/gem_version.rb b/actioncable/lib/action_cable/gem_version.rb index 6e7053e32ea20..fd6a9c80aabce 100644 --- a/actioncable/lib/action_cable/gem_version.rb +++ b/actioncable/lib/action_cable/gem_version.rb @@ -1,13 +1,15 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable - # Returns the version of the currently loaded Action Cable as a Gem::Version. + # Returns the currently loaded version of Action Cable as a `Gem::Version`. def self.gem_version Gem::Version.new VERSION::STRING end module VERSION - MAJOR = 6 + MAJOR = 8 MINOR = 1 TINY = 0 PRE = "alpha" diff --git a/actioncable/lib/action_cable/helpers/action_cable_helper.rb b/actioncable/lib/action_cable/helpers/action_cable_helper.rb index df16c02e837e7..93d21a983cef8 100644 --- a/actioncable/lib/action_cable/helpers/action_cable_helper.rb +++ b/actioncable/lib/action_cable/helpers/action_cable_helper.rb @@ -1,34 +1,37 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module Helpers module ActionCableHelper - # Returns an "action-cable-url" meta tag with the value of the URL specified in your - # configuration. Ensure this is above your JavaScript tag: + # Returns an "action-cable-url" meta tag with the value of the URL specified in + # your configuration. Ensure this is above your JavaScript tag: # - # - # <%= action_cable_meta_tag %> - # <%= javascript_include_tag 'application', 'data-turbolinks-track' => 'reload' %> - # + # + # <%= action_cable_meta_tag %> + # <%= javascript_include_tag 'application', 'data-turbo-track' => 'reload' %> + # # - # This is then used by Action Cable to determine the URL of your WebSocket server. - # Your CoffeeScript can then connect to the server without needing to specify the - # URL directly: + # This is then used by Action Cable to determine the URL of your WebSocket + # server. Your JavaScript can then connect to the server without needing to + # specify the URL directly: # - # #= require cable - # @App = {} - # App.cable = Cable.createConsumer() + # import Cable from "@rails/actioncable" + # window.Cable = Cable + # window.App = {} + # App.cable = Cable.createConsumer() # # Make sure to specify the correct server location in each of your environment # config files: # - # config.action_cable.mount_path = "/cable123" - # <%= action_cable_meta_tag %> would render: - # => + # config.action_cable.mount_path = "/cable123" + # <%= action_cable_meta_tag %> would render: + # => # - # config.action_cable.url = "ws://actioncable.com" - # <%= action_cable_meta_tag %> would render: - # => + # config.action_cable.url = "ws://actioncable.com" + # <%= action_cable_meta_tag %> would render: + # => # def action_cable_meta_tag tag "meta", name: "action-cable-url", content: ( diff --git a/actioncable/lib/action_cable/remote_connections.rb b/actioncable/lib/action_cable/remote_connections.rb index 283400d9e7add..e167a1c5521c8 100644 --- a/actioncable/lib/action_cable/remote_connections.rb +++ b/actioncable/lib/action_cable/remote_connections.rb @@ -1,24 +1,33 @@ # frozen_string_literal: true +# :markup: markdown + require "active_support/core_ext/module/redefine_method" module ActionCable + # # Action Cable Remote Connections + # # If you need to disconnect a given connection, you can go through the # RemoteConnections. You can find the connections you're looking for by # searching for the identifier declared on the connection. For example: # - # module ApplicationCable - # class Connection < ActionCable::Connection::Base - # identified_by :current_user - # .... + # module ApplicationCable + # class Connection < ActionCable::Connection::Base + # identified_by :current_user + # .... + # end # end - # end # - # ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect + # ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect + # + # This will disconnect all the connections established for `User.find(1)`, + # across all servers running on all machines, because it uses the internal + # channel that all of these servers are subscribed to. + # + # By default, server sends a "disconnect" message with "reconnect" flag set to + # true. You can override it by specifying the `reconnect` option: # - # This will disconnect all the connections established for - # User.find(1), across all servers running on all machines, because - # it uses the internal channel that all of these servers are subscribed to. + # ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect(reconnect: false) class RemoteConnections attr_reader :server @@ -30,42 +39,44 @@ def where(identifier) RemoteConnection.new(server, identifier) end - private - # Represents a single remote connection found via ActionCable.server.remote_connections.where(*). - # Exists solely for the purpose of calling #disconnect on that connection. - class RemoteConnection - class InvalidIdentifiersError < StandardError; end + # # Action Cable Remote Connection + # + # Represents a single remote connection found via + # `ActionCable.server.remote_connections.where(*)`. Exists solely for the + # purpose of calling #disconnect on that connection. + class RemoteConnection + class InvalidIdentifiersError < StandardError; end - include Connection::Identification, Connection::InternalChannel + include Connection::Identification, Connection::InternalChannel - def initialize(server, ids) - @server = server - set_identifier_instance_vars(ids) - end + def initialize(server, ids) + @server = server + set_identifier_instance_vars(ids) + end - # Uses the internal channel to disconnect the connection. - def disconnect - server.broadcast internal_channel, type: "disconnect" - end + # Uses the internal channel to disconnect the connection. + def disconnect(reconnect: true) + server.broadcast internal_channel, { type: "disconnect", reconnect: reconnect } + end - # Returns all the identifiers that were applied to this connection. - redefine_method :identifiers do - server.connection_identifiers - end + # Returns all the identifiers that were applied to this connection. + redefine_method :identifiers do + server.connection_identifiers + end - protected - attr_reader :server + protected + attr_reader :server - private - def set_identifier_instance_vars(ids) - raise InvalidIdentifiersError unless valid_identifiers?(ids) - ids.each { |k, v| instance_variable_set("@#{k}", v) } - end + private + def set_identifier_instance_vars(ids) + raise InvalidIdentifiersError unless valid_identifiers?(ids) + ids.each { |k, v| instance_variable_set("@#{k}", v) } + end - def valid_identifiers?(ids) - keys = ids.keys - identifiers.all? { |id| keys.include?(id) } - end - end + def valid_identifiers?(ids) + keys = ids.keys + identifiers.all? { |id| keys.include?(id) } + end + end end end diff --git a/actioncable/lib/action_cable/server.rb b/actioncable/lib/action_cable/server.rb deleted file mode 100644 index 8d485a44f6c75..0000000000000 --- a/actioncable/lib/action_cable/server.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module ActionCable - module Server - extend ActiveSupport::Autoload - - eager_autoload do - autoload :Base - autoload :Broadcasting - autoload :Connections - autoload :Configuration - - autoload :Worker - autoload :ActiveRecordConnectionManagement, "action_cable/server/worker/active_record_connection_management" - end - end -end diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb index 4f0813e30f717..cd150700e2329 100644 --- a/actioncable/lib/action_cable/server/base.rb +++ b/actioncable/lib/action_cable/server/base.rb @@ -1,13 +1,20 @@ # frozen_string_literal: true +# :markup: markdown + require "monitor" module ActionCable module Server - # A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the Rack process that starts the Action Cable server, but - # is also used by the user to reach the RemoteConnections object, which is used for finding and disconnecting connections across all servers. + # # Action Cable Server Base + # + # A singleton ActionCable::Server instance is available via ActionCable.server. + # It's used by the Rack process that starts the Action Cable server, but is also + # used by the user to reach the RemoteConnections object, which is used for + # finding and disconnecting connections across all servers. # - # Also, this is the server instance used for broadcasting. See Broadcasting for more information. + # Also, this is the server instance used for broadcasting. See Broadcasting for + # more information. class Base include ActionCable::Server::Broadcasting include ActionCable::Server::Connections @@ -29,11 +36,13 @@ def initialize(config: self.class.config) # Called by Rack to set up the server. def call(env) + return config.health_check_application.call(env) if env["PATH_INFO"] == config.health_check_path setup_heartbeat_timer config.connection_class.call.new(self, env).process end - # Disconnect all the connections identified by +identifiers+ on this server or any others via RemoteConnections. + # Disconnect all the connections identified by `identifiers` on this server or + # any others via RemoteConnections. def disconnect(identifiers) remote_connections.where(identifiers).disconnect end @@ -63,17 +72,22 @@ def event_loop @event_loop || @mutex.synchronize { @event_loop ||= ActionCable::Connection::StreamEventLoop.new } end - # The worker pool is where we run connection callbacks and channel actions. We do as little as possible on the server's main thread. - # The worker pool is an executor service that's backed by a pool of threads working from a task queue. The thread pool size maxes out - # at 4 worker threads by default. Tune the size yourself with config.action_cable.worker_pool_size. + # The worker pool is where we run connection callbacks and channel actions. We + # do as little as possible on the server's main thread. The worker pool is an + # executor service that's backed by a pool of threads working from a task queue. + # The thread pool size maxes out at 4 worker threads by default. Tune the size + # yourself with `config.action_cable.worker_pool_size`. # - # Using Active Record, Redis, etc within your channel actions means you'll get a separate connection from each thread in the worker pool. - # Plan your deployment accordingly: 5 servers each running 5 Puma workers each running an 8-thread worker pool means at least 200 database - # connections. + # Using Active Record, Redis, etc within your channel actions means you'll get a + # separate connection from each thread in the worker pool. Plan your deployment + # accordingly: 5 servers each running 5 Puma workers each running an 8-thread + # worker pool means at least 200 database connections. # - # Also, ensure that your database connection pool size is as least as large as your worker pool size. Otherwise, workers may oversubscribe - # the database connection pool and block while they wait for other workers to release their connections. Use a smaller worker pool or a larger - # database connection pool instead. + # Also, ensure that your database connection pool size is as least as large as + # your worker pool size. Otherwise, workers may oversubscribe the database + # connection pool and block while they wait for other workers to release their + # connections. Use a smaller worker pool or a larger database connection pool + # instead. def worker_pool @worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) } end @@ -83,7 +97,8 @@ def pubsub @pubsub || @mutex.synchronize { @pubsub ||= config.pubsub_adapter.new(self) } end - # All of the identifiers applied to the connection class associated with this server. + # All of the identifiers applied to the connection class associated with this + # server. def connection_identifiers config.connection_class.call.identifiers end diff --git a/actioncable/lib/action_cable/server/broadcasting.rb b/actioncable/lib/action_cable/server/broadcasting.rb index b73cfa7d1f177..cedd0fb58fb0b 100644 --- a/actioncable/lib/action_cable/server/broadcasting.rb +++ b/actioncable/lib/action_cable/server/broadcasting.rb @@ -1,32 +1,42 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module Server - # Broadcasting is how other parts of your application can send messages to a channel's subscribers. As explained in Channel, most of the time, these - # broadcastings are streamed directly to the clients subscribed to the named broadcasting. Let's explain with a full-stack example: + # # Action Cable Server Broadcasting + # + # Broadcasting is how other parts of your application can send messages to a + # channel's subscribers. As explained in Channel, most of the time, these + # broadcastings are streamed directly to the clients subscribed to the named + # broadcasting. Let's explain with a full-stack example: # - # class WebNotificationsChannel < ApplicationCable::Channel - # def subscribed - # stream_from "web_notifications_#{current_user.id}" + # class WebNotificationsChannel < ApplicationCable::Channel + # def subscribed + # stream_from "web_notifications_#{current_user.id}" + # end # end - # end # - # # Somewhere in your app this is called, perhaps from a NewCommentJob: - # ActionCable.server.broadcast \ - # "web_notifications_1", { title: "New things!", body: "All that's fit for print" } + # # Somewhere in your app this is called, perhaps from a NewCommentJob: + # ActionCable.server.broadcast \ + # "web_notifications_1", { title: "New things!", body: "All that's fit for print" } # - # # Client-side CoffeeScript, which assumes you've already requested the right to send web notifications: - # App.cable.subscriptions.create "WebNotificationsChannel", - # received: (data) -> - # new Notification data['title'], body: data['body'] + # # Client-side JavaScript, which assumes you've already requested the right to send web notifications: + # App.cable.subscriptions.create("WebNotificationsChannel", { + # received: function(data) { + # new Notification(data['title'], { body: data['body'] }) + # } + # }) module Broadcasting - # Broadcast a hash directly to a named broadcasting. This will later be JSON encoded. + # Broadcast a hash directly to a named `broadcasting`. This will later be JSON + # encoded. def broadcast(broadcasting, message, coder: ActiveSupport::JSON) broadcaster_for(broadcasting, coder: coder).broadcast(message) end - # Returns a broadcaster for a named broadcasting that can be reused. Useful when you have an object that - # may need multiple spots to transmit to a specific broadcasting over and over. + # Returns a broadcaster for a named `broadcasting` that can be reused. Useful + # when you have an object that may need multiple spots to transmit to a specific + # broadcasting over and over. def broadcaster_for(broadcasting, coder: ActiveSupport::JSON) Broadcaster.new(self, String(broadcasting), coder: coder) end @@ -40,7 +50,7 @@ def initialize(server, broadcasting, coder:) end def broadcast(message) - server.logger.debug { "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}" } + server.logger.debug { "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect.truncate(300)}" } payload = { broadcasting: broadcasting, message: message, coder: coder } ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb index 26209537df378..6210bfcf47b9d 100644 --- a/actioncable/lib/action_cable/server/configuration.rb +++ b/actioncable/lib/action_cable/server/configuration.rb @@ -1,14 +1,23 @@ # frozen_string_literal: true +# :markup: markdown + +require "rack" + module ActionCable module Server - # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak Action Cable configuration - # in a Rails config initializer. + # # Action Cable Server Configuration + # + # An instance of this configuration object is available via + # ActionCable.server.config, which allows you to tweak Action Cable + # configuration in a Rails config initializer. class Configuration attr_accessor :logger, :log_tags attr_accessor :connection_class, :worker_pool_size - attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host + attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host, :filter_parameters attr_accessor :cable, :url, :mount_path + attr_accessor :precompile_assets + attr_accessor :health_check_path, :health_check_application def initialize @log_tags = [] @@ -18,30 +27,35 @@ def initialize @disable_request_forgery_protection = false @allow_same_origin_as_host = true + @filter_parameters = [] + + @health_check_application = ->(env) { + [200, { Rack::CONTENT_TYPE => "text/html", "date" => Time.now.httpdate }, []] + } end - # Returns constant of subscription adapter specified in config/cable.yml. - # If the adapter cannot be found, this will default to the Redis adapter. - # Also makes sure proper dependencies are required. + # Returns constant of subscription adapter specified in config/cable.yml. If the + # adapter cannot be found, this will default to the Redis adapter. Also makes + # sure proper dependencies are required. def pubsub_adapter adapter = (cable.fetch("adapter") { "redis" }) # Require the adapter itself and give useful feedback about - # 1. Missing adapter gems and - # 2. Adapter gems' missing dependencies. + # 1. Missing adapter gems and + # 2. Adapter gems' missing dependencies. path_to_adapter = "action_cable/subscription_adapter/#{adapter}" begin require path_to_adapter rescue LoadError => e - # We couldn't require the adapter itself. Raise an exception that - # points out config typos and missing gems. + # We couldn't require the adapter itself. Raise an exception that points out + # config typos and missing gems. if e.path == path_to_adapter - # We can assume that a non-builtin adapter was specified, so it's - # either misspelled or missing from Gemfile. + # We can assume that a non-builtin adapter was specified, so it's either + # misspelled or missing from Gemfile. raise e.class, "Could not load the '#{adapter}' Action Cable pubsub adapter. Ensure that the adapter is spelled correctly in config/cable.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace - # Bubbled up from the adapter require. Prefix the exception message - # with some guidance about how to address it and reraise. + # Bubbled up from the adapter require. Prefix the exception message with some + # guidance about how to address it and reraise. else raise e.class, "Error loading the '#{adapter}' Action Cable pubsub adapter. Missing a gem it depends on? #{e.message}", e.backtrace end diff --git a/actioncable/lib/action_cable/server/connections.rb b/actioncable/lib/action_cable/server/connections.rb index 39557d63a79e5..e51933b177410 100644 --- a/actioncable/lib/action_cable/server/connections.rb +++ b/actioncable/lib/action_cable/server/connections.rb @@ -1,9 +1,15 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module Server - # Collection class for all the connections that have been established on this specific server. Remember, usually you'll run many Action Cable servers, so - # you can't use this collection as a full list of all of the connections established against your application. Instead, use RemoteConnections for that. + # # Action Cable Server Connections + # + # Collection class for all the connections that have been established on this + # specific server. Remember, usually you'll run many Action Cable servers, so + # you can't use this collection as a full list of all of the connections + # established against your application. Instead, use RemoteConnections for that. module Connections # :nodoc: BEAT_INTERVAL = 3 @@ -19,12 +25,14 @@ def remove_connection(connection) connections.delete connection end - # WebSocket connection implementations differ on when they'll mark a connection as stale. We basically never want a connection to go stale, as you - # then can't rely on being able to communicate with the connection. To solve this, a 3 second heartbeat runs on all connections. If the beat fails, we automatically + # WebSocket connection implementations differ on when they'll mark a connection + # as stale. We basically never want a connection to go stale, as you then can't + # rely on being able to communicate with the connection. To solve this, a 3 + # second heartbeat runs on all connections. If the beat fails, we automatically # disconnect. def setup_heartbeat_timer @heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do - event_loop.post { connections.map(&:beat) } + event_loop.post { connections.each(&:beat) } end end diff --git a/actioncable/lib/action_cable/server/worker.rb b/actioncable/lib/action_cable/server/worker.rb index b918d4ae7278d..535849a1515e7 100644 --- a/actioncable/lib/action_cable/server/worker.rb +++ b/actioncable/lib/action_cable/server/worker.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + require "active_support/callbacks" require "active_support/core_ext/module/attribute_accessors_per_thread" require "concurrent" @@ -18,14 +20,15 @@ class Worker # :nodoc: def initialize(max_size: 5) @executor = Concurrent::ThreadPoolExecutor.new( + name: "ActionCable-server", min_threads: 1, max_threads: max_size, max_queue: 0, ) end - # Stop processing work: any work that has not already started - # running will be discarded from the queue + # Stop processing work: any work that has not already started running will be + # discarded from the queue def halt @executor.shutdown end @@ -34,12 +37,10 @@ def stopping? @executor.shuttingdown? end - def work(connection) + def work(connection, &block) self.connection = connection - run_callbacks :work do - yield - end + run_callbacks :work, &block ensure self.connection = nil end diff --git a/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb index 2e378d4bf317b..2512c500f4265 100644 --- a/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb +++ b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module Server class Worker @@ -12,8 +14,8 @@ module ActiveRecordConnectionManagement end end - def with_database_connections - connection.logger.tag(ActiveRecord::Base.logger) { yield } + def with_database_connections(&block) + connection.logger.tag(ActiveRecord::Base.logger, &block) end end end diff --git a/actioncable/lib/action_cable/subscription_adapter.rb b/actioncable/lib/action_cable/subscription_adapter.rb deleted file mode 100644 index 6a9d5c208052a..0000000000000 --- a/actioncable/lib/action_cable/subscription_adapter.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module ActionCable - module SubscriptionAdapter - extend ActiveSupport::Autoload - - autoload :Base - autoload :Test - autoload :SubscriberMap - autoload :ChannelPrefix - end -end diff --git a/actioncable/lib/action_cable/subscription_adapter/async.rb b/actioncable/lib/action_cable/subscription_adapter/async.rb index c9930299c7e31..f78edb9ae7c35 100644 --- a/actioncable/lib/action_cable/subscription_adapter/async.rb +++ b/actioncable/lib/action_cable/subscription_adapter/async.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "action_cable/subscription_adapter/inline" +# :markup: markdown module ActionCable module SubscriptionAdapter diff --git a/actioncable/lib/action_cable/subscription_adapter/base.rb b/actioncable/lib/action_cable/subscription_adapter/base.rb index 4d3d6ee913f12..2df2667b2336c 100644 --- a/actioncable/lib/action_cable/subscription_adapter/base.rb +++ b/actioncable/lib/action_cable/subscription_adapter/base.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module SubscriptionAdapter class Base @@ -27,7 +29,8 @@ def shutdown end def identifier - @server.config.cable[:id] ||= "ActionCable-PID-#{$$}" + @server.config.cable[:id] = "ActionCable-PID-#{$$}" unless @server.config.cable.key?(:id) + @server.config.cable[:id] end end end diff --git a/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb b/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb index df0aa040f5ba2..2c3e88f307ed2 100644 --- a/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb +++ b/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module SubscriptionAdapter module ChannelPrefix # :nodoc: diff --git a/actioncable/lib/action_cable/subscription_adapter/inline.rb b/actioncable/lib/action_cable/subscription_adapter/inline.rb index d2c85c1c8d582..88559fb39c64e 100644 --- a/actioncable/lib/action_cable/subscription_adapter/inline.rb +++ b/actioncable/lib/action_cable/subscription_adapter/inline.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module SubscriptionAdapter class Inline < Base # :nodoc: diff --git a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb index 84022e2bdcda7..32c63bbebf50a 100644 --- a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb +++ b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true -gem "pg", ">= 0.18", "< 2.0" +# :markup: markdown + +gem "pg", "~> 1.1" require "pg" -require "thread" -require "digest/sha1" +require "openssl" module ActionCable module SubscriptionAdapter @@ -34,18 +35,17 @@ def shutdown end def with_subscriptions_connection(&block) # :nodoc: - ar_conn = ActiveRecord::Base.connection_pool.checkout.tap do |conn| - # Action Cable is taking ownership over this database connection, and - # will perform the necessary cleanup tasks - ActiveRecord::Base.connection_pool.remove(conn) - end + # Action Cable is taking ownership over this database connection, and will + # perform the necessary cleanup tasks. + # We purposedly avoid #checkout to not end up with a pinned connection + ar_conn = ActiveRecord::Base.connection_pool.new_connection pg_conn = ar_conn.raw_connection verify!(pg_conn) pg_conn.exec("SET application_name = #{pg_conn.escape_identifier(identifier)}") yield pg_conn ensure - ar_conn.disconnect! + ar_conn&.disconnect! end def with_broadcast_connection(&block) # :nodoc: @@ -58,7 +58,7 @@ def with_broadcast_connection(&block) # :nodoc: private def channel_identifier(channel) - channel.size > 63 ? Digest::SHA1.hexdigest(channel) : channel + channel.size > 63 ? OpenSSL::Digest::SHA1.hexdigest(channel) : channel end def listener diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb index 9093395ae966b..da58e8652ce8c 100644 --- a/actioncable/lib/action_cable/subscription_adapter/redis.rb +++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require "thread" +# :markup: markdown -gem "redis", ">= 3", "< 5" +gem "redis", ">= 4", "< 6" require "redis" require "active_support/core_ext/hash/except" @@ -12,8 +12,9 @@ module SubscriptionAdapter class Redis < Base # :nodoc: prepend ChannelPrefix - # Overwrite this factory method for Redis connections if you want to use a different Redis library than the redis gem. - # This is needed, for example, when using Makara proxies for distributed Redis. + # Overwrite this factory method for Redis connections if you want to use a + # different Redis library than the redis gem. This is needed, for example, when + # using Makara proxies for distributed Redis. cattr_accessor :redis_connector, default: ->(config) do ::Redis.new(config.except(:adapter, :channel_prefix)) end @@ -46,7 +47,7 @@ def redis_connection_for_subscriptions private def listener - @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) } + @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, config_options, @server.event_loop) } end def redis_connection_for_broadcasts @@ -56,11 +57,15 @@ def redis_connection_for_broadcasts end def redis_connection - self.class.redis_connector.call(@server.config.cable.merge(id: identifier)) + self.class.redis_connector.call(config_options) + end + + def config_options + @config_options ||= @server.config.cable.deep_symbolize_keys.merge(id: identifier) end class Listener < SubscriberMap - def initialize(adapter, event_loop) + def initialize(adapter, config_options, event_loop) super() @adapter = adapter @@ -69,7 +74,12 @@ def initialize(adapter, event_loop) @subscribe_callbacks = Hash.new { |h, k| h[k] = [] } @subscription_lock = Mutex.new - @raw_client = nil + @reconnect_attempt = 0 + # Use the same config as used by Redis conn + @reconnect_attempts = config_options.fetch(:reconnect_attempts, 1) + @reconnect_attempts = Array.new(@reconnect_attempts, 0) if @reconnect_attempts.is_a?(Integer) + + @subscribed_client = nil @when_connected = [] @@ -78,13 +88,14 @@ def initialize(adapter, event_loop) def listen(conn) conn.without_reconnect do - original_client = conn.respond_to?(:_client) ? conn._client : conn.client + original_client = extract_subscribed_client(conn) conn.subscribe("_action_cable_internal") do |on| on.subscribe do |chan, count| @subscription_lock.synchronize do if count == 1 - @raw_client = original_client + @reconnect_attempt = 0 + @subscribed_client = original_client until @when_connected.empty? @when_connected.shift.call @@ -106,7 +117,7 @@ def listen(conn) on.unsubscribe do |chan, count| if count == 0 @subscription_lock.synchronize do - @raw_client = nil + @subscribed_client = nil end end end @@ -119,8 +130,8 @@ def shutdown return if @thread.nil? when_connected do - send_command("unsubscribe") - @raw_client = nil + @subscribed_client.unsubscribe + @subscribed_client = nil end end @@ -131,13 +142,13 @@ def add_channel(channel, on_success) @subscription_lock.synchronize do ensure_listener_running @subscribe_callbacks[channel] << on_success - when_connected { send_command("subscribe", channel) } + when_connected { @subscribed_client.subscribe(channel) } end end def remove_channel(channel) @subscription_lock.synchronize do - when_connected { send_command("unsubscribe", channel) } + when_connected { @subscribed_client.unsubscribe(channel) } end end @@ -150,28 +161,93 @@ def ensure_listener_running @thread ||= Thread.new do Thread.current.abort_on_exception = true - conn = @adapter.redis_connection_for_subscriptions - listen conn + begin + conn = @adapter.redis_connection_for_subscriptions + listen conn + rescue ConnectionError + reset + if retry_connecting? + when_connected { resubscribe } + retry + end + end end end def when_connected(&block) - if @raw_client + if @subscribed_client block.call else @when_connected << block end end - def send_command(*command) - @raw_client.write(command) + def retry_connecting? + @reconnect_attempt += 1 + + return false if @reconnect_attempt > @reconnect_attempts.size + + sleep_t = @reconnect_attempts[@reconnect_attempt - 1] + + sleep(sleep_t) if sleep_t > 0 + + true + end + + def resubscribe + channels = @sync.synchronize do + @subscribers.keys + end + @subscribed_client.subscribe(*channels) unless channels.empty? + end + + def reset + @subscription_lock.synchronize do + @subscribed_client = nil + @subscribe_callbacks.clear + @when_connected.clear + end + end + + if ::Redis::VERSION < "5" + ConnectionError = ::Redis::BaseConnectionError + + class SubscribedClient + def initialize(raw_client) + @raw_client = raw_client + end + + def subscribe(*channel) + send_command("subscribe", *channel) + end + + def unsubscribe(*channel) + send_command("unsubscribe", *channel) + end - very_raw_connection = - @raw_client.connection.instance_variable_defined?(:@connection) && - @raw_client.connection.instance_variable_get(:@connection) + private + def send_command(*command) + @raw_client.write(command) + + very_raw_connection = + @raw_client.connection.instance_variable_defined?(:@connection) && + @raw_client.connection.instance_variable_get(:@connection) + + if very_raw_connection && very_raw_connection.respond_to?(:flush) + very_raw_connection.flush + end + nil + end + end + + def extract_subscribed_client(conn) + SubscribedClient.new(conn._client) + end + else + ConnectionError = RedisClient::ConnectionError - if very_raw_connection && very_raw_connection.respond_to?(:flush) - very_raw_connection.flush + def extract_subscribed_client(conn) + conn end end end diff --git a/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb b/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb index 01cdc2dfa1860..d541681d3d473 100644 --- a/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb +++ b/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module SubscriptionAdapter class SubscriberMap diff --git a/actioncable/lib/action_cable/subscription_adapter/test.rb b/actioncable/lib/action_cable/subscription_adapter/test.rb index ce604cc88eb71..d09018aabbab5 100644 --- a/actioncable/lib/action_cable/subscription_adapter/test.rb +++ b/actioncable/lib/action_cable/subscription_adapter/test.rb @@ -1,18 +1,19 @@ # frozen_string_literal: true -require_relative "async" +# :markup: markdown module ActionCable module SubscriptionAdapter - # == Test adapter for Action Cable + # ## Test adapter for Action Cable # # The test adapter should be used only in testing. Along with - # ActionCable::TestHelper it makes a great tool to test your Rails application. + # ActionCable::TestHelper it makes a great tool to test your Rails application. # - # To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file. + # To use the test adapter set `adapter` value to `test` in your + # `config/cable.yml` file. # - # NOTE: Test adapter extends the ActionCable::SubscriptionsAdapter::Async adapter, - # so it could be used in system tests too. + # NOTE: `Test` adapter extends the `ActionCable::SubscriptionAdapter::Async` + # adapter, so it could be used in system tests too. class Test < Async def broadcast(channel, payload) broadcasts(channel) << payload diff --git a/actioncable/lib/action_cable/test_case.rb b/actioncable/lib/action_cable/test_case.rb index d153259bf63e0..b56f2ead6c306 100644 --- a/actioncable/lib/action_cable/test_case.rb +++ b/actioncable/lib/action_cable/test_case.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + require "active_support/test_case" module ActionCable diff --git a/actioncable/lib/action_cable/test_helper.rb b/actioncable/lib/action_cable/test_helper.rb index 3d709f651940c..682d8fc039843 100644 --- a/actioncable/lib/action_cable/test_helper.rb +++ b/actioncable/lib/action_cable/test_helper.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable # Provides helper methods for testing Action Cable broadcasting module TestHelper @@ -18,105 +20,123 @@ def after_teardown # :nodoc: ActionCable.server.instance_variable_set(:@pubsub, @old_pubsub_adapter) end - # Asserts that the number of broadcasted messages to the stream matches the given number. - # - # def test_broadcasts - # assert_broadcasts 'messages', 0 - # ActionCable.server.broadcast 'messages', { text: 'hello' } - # assert_broadcasts 'messages', 1 - # ActionCable.server.broadcast 'messages', { text: 'world' } - # assert_broadcasts 'messages', 2 - # end - # - # If a block is passed, that block should cause the specified number of - # messages to be broadcasted. + # Asserts that the number of broadcasted messages to the stream matches the + # given number. # - # def test_broadcasts_again - # assert_broadcasts('messages', 1) do + # def test_broadcasts + # assert_broadcasts 'messages', 0 # ActionCable.server.broadcast 'messages', { text: 'hello' } + # assert_broadcasts 'messages', 1 + # ActionCable.server.broadcast 'messages', { text: 'world' } + # assert_broadcasts 'messages', 2 # end # - # assert_broadcasts('messages', 2) do - # ActionCable.server.broadcast 'messages', { text: 'hi' } - # ActionCable.server.broadcast 'messages', { text: 'how are you?' } + # If a block is passed, that block should cause the specified number of messages + # to be broadcasted. + # + # def test_broadcasts_again + # assert_broadcasts('messages', 1) do + # ActionCable.server.broadcast 'messages', { text: 'hello' } + # end + # + # assert_broadcasts('messages', 2) do + # ActionCable.server.broadcast 'messages', { text: 'hi' } + # ActionCable.server.broadcast 'messages', { text: 'how are you?' } + # end # end - # end # def assert_broadcasts(stream, number, &block) if block_given? - original_count = broadcasts_size(stream) - assert_nothing_raised(&block) - new_count = broadcasts_size(stream) - actual_count = new_count - original_count + new_messages = new_broadcasts_from(broadcasts(stream), stream, "assert_broadcasts", &block) + + actual_count = new_messages.size + assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent" else - actual_count = broadcasts_size(stream) + actual_count = broadcasts(stream).size + assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent" end - - assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent" end # Asserts that no messages have been sent to the stream. # - # def test_no_broadcasts - # assert_no_broadcasts 'messages' - # ActionCable.server.broadcast 'messages', { text: 'hi' } - # assert_broadcasts 'messages', 1 - # end + # def test_no_broadcasts + # assert_no_broadcasts 'messages' + # ActionCable.server.broadcast 'messages', { text: 'hi' } + # assert_broadcasts 'messages', 1 + # end # # If a block is passed, that block should not cause any message to be sent. # - # def test_broadcasts_again - # assert_no_broadcasts 'messages' do - # # No job messages should be sent from this block + # def test_broadcasts_again + # assert_no_broadcasts 'messages' do + # # No job messages should be sent from this block + # end # end - # end # # Note: This assertion is simply a shortcut for: # - # assert_broadcasts 'messages', 0, &block + # assert_broadcasts 'messages', 0, &block # def assert_no_broadcasts(stream, &block) assert_broadcasts stream, 0, &block end - # Asserts that the specified message has been sent to the stream. + # Returns the messages that are broadcasted in the block. # - # def test_assert_transmitted_message - # ActionCable.server.broadcast 'messages', text: 'hello' - # assert_broadcast_on('messages', text: 'hello') - # end + # def test_broadcasts + # messages = capture_broadcasts('messages') do + # ActionCable.server.broadcast 'messages', { text: 'hi' } + # ActionCable.server.broadcast 'messages', { text: 'how are you?' } + # end + # assert_equal 2, messages.length + # assert_equal({ text: 'hi' }, messages.first) + # assert_equal({ text: 'how are you?' }, messages.last) + # end # - # If a block is passed, that block should cause a message with the specified data to be sent. + def capture_broadcasts(stream, &block) + new_broadcasts_from(broadcasts(stream), stream, "capture_broadcasts", &block).map { |m| ActiveSupport::JSON.decode(m) } + end + + # Asserts that the specified message has been sent to the stream. # - # def test_assert_broadcast_on_again - # assert_broadcast_on('messages', text: 'hello') do + # def test_assert_transmitted_message # ActionCable.server.broadcast 'messages', text: 'hello' + # assert_broadcast_on('messages', text: 'hello') + # end + # + # If a block is passed, that block should cause a message with the specified + # data to be sent. + # + # def test_assert_broadcast_on_again + # assert_broadcast_on('messages', text: 'hello') do + # ActionCable.server.broadcast 'messages', text: 'hello' + # end # end - # end # def assert_broadcast_on(stream, data, &block) - # Encode to JSON and back–we want to use this value to compare - # with decoded JSON. - # Comparing JSON strings doesn't work due to the order if the keys. + # Encode to JSON and back–we want to use this value to compare with decoded + # JSON. Comparing JSON strings doesn't work due to the order if the keys. serialized_msg = ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data)) new_messages = broadcasts(stream) if block_given? - old_messages = new_messages - clear_messages(stream) - - assert_nothing_raised(&block) - new_messages = broadcasts(stream) - clear_messages(stream) - - # Restore all sent messages - (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) } + new_messages = new_broadcasts_from(new_messages, stream, "assert_broadcast_on", &block) end message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg } - assert message, "No messages sent with #{data} to #{stream}" + error_message = "No messages sent with #{data} to #{stream}" + + if new_messages.any? + error_message = new_messages.inject("#{error_message}\nMessage(s) found:\n") do |error_message, new_message| + error_message + "#{ActiveSupport::JSON.decode(new_message)}\n" + end + else + error_message = "#{error_message}\nNo message found for #{stream}" + end + + assert message, error_message end def pubsub_adapter # :nodoc: @@ -126,8 +146,18 @@ def pubsub_adapter # :nodoc: delegate :broadcasts, :clear_messages, to: :pubsub_adapter private - def broadcasts_size(channel) - broadcasts(channel).size + def new_broadcasts_from(current_messages, stream, assertion, &block) + old_messages = current_messages + clear_messages(stream) + + _assert_nothing_raised_or_warn(assertion, &block) + new_messages = broadcasts(stream) + clear_messages(stream) + + # Restore all sent messages + (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) } + + new_messages end end end diff --git a/actioncable/lib/action_cable/version.rb b/actioncable/lib/action_cable/version.rb index 86115c6065208..14423f9a6a1b6 100644 --- a/actioncable/lib/action_cable/version.rb +++ b/actioncable/lib/action_cable/version.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true +# :markup: markdown + require_relative "gem_version" module ActionCable - # Returns the version of the currently loaded Action Cable as a Gem::Version + # Returns the currently loaded version of Action Cable as a `Gem::Version`. def self.version gem_version end diff --git a/actioncable/lib/rails/generators/channel/USAGE b/actioncable/lib/rails/generators/channel/USAGE index 32456678d96cc..13190920227e3 100644 --- a/actioncable/lib/rails/generators/channel/USAGE +++ b/actioncable/lib/rails/generators/channel/USAGE @@ -1,13 +1,19 @@ Description: -============ - Stubs out a new cable channel for the server (in Ruby) and client (in JavaScript). + Generates a new cable channel for the server (in Ruby) and client (in JavaScript). Pass the channel name, either CamelCased or under_scored, and an optional list of channel actions as arguments. -Example: -======== - bin/rails generate channel Chat speak +Examples: + `bin/rails generate channel notification` - creates a Chat channel class, test and JavaScript asset: - Channel: app/channels/chat_channel.rb - Test: test/channels/chat_channel_test.rb - Assets: app/javascript/channels/chat_channel.js + creates a notification channel class, test and JavaScript asset: + Channel: app/channels/notification_channel.rb + Test: test/channels/notification_channel_test.rb + Assets: $JAVASCRIPT_PATH/channels/notification_channel.js + + `bin/rails generate channel chat speak` + + creates a chat channel with a speak action. + + `bin/rails generate channel comments --no-assets` + + creates a comments channel without JavaScript assets. diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb index 0b80d1f96b936..72a6a3821ebe8 100644 --- a/actioncable/lib/rails/generators/channel/channel_generator.rb +++ b/actioncable/lib/rails/generators/channel/channel_generator.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module Rails module Generators class ChannelGenerator < NamedBase @@ -13,39 +15,112 @@ class ChannelGenerator < NamedBase hook_for :test_framework - def create_channel_file - template "channel.rb", File.join("app/channels", class_path, "#{file_name}_channel.rb") + def create_channel_files + create_shared_channel_files + create_channel_file + + if using_javascript? + if first_setup_required? + create_shared_channel_javascript_files + import_channels_in_javascript_entrypoint - if options[:assets] - if behavior == :invoke - template "javascript/index.js", "app/javascript/channels/index.js" - template "javascript/consumer.js", "app/javascript/channels/consumer.js" + if using_importmap? + pin_javascript_dependencies + elsif using_js_runtime? + install_javascript_dependencies + end end - js_template "javascript/channel", File.join("app/javascript/channels", class_path, "#{file_name}_channel") + create_channel_javascript_file + import_channel_in_javascript_entrypoint end - - generate_application_cable_files end private + def create_shared_channel_files + return if behavior != :invoke + + copy_file "#{__dir__}/templates/application_cable/channel.rb", + "app/channels/application_cable/channel.rb" + copy_file "#{__dir__}/templates/application_cable/connection.rb", + "app/channels/application_cable/connection.rb" + end + + def create_channel_file + template "channel.rb", + File.join("app/channels", class_path, "#{file_name}_channel.rb") + end + + def create_shared_channel_javascript_files + template "javascript/index.js", "app/javascript/channels/index.js" + template "javascript/consumer.js", "app/javascript/channels/consumer.js" + end + + def create_channel_javascript_file + channel_js_path = File.join("app/javascript/channels", class_path, "#{file_name}_channel") + js_template "javascript/channel", channel_js_path + gsub_file "#{channel_js_path}.js", /\.\/consumer/, "channels/consumer" if using_importmap? + end + + def import_channels_in_javascript_entrypoint + append_to_file "app/javascript/application.js", + using_importmap? ? %(import "channels"\n) : %(import "./channels"\n) + end + + def import_channel_in_javascript_entrypoint + append_to_file "app/javascript/channels/index.js", + using_importmap? ? %(import "channels/#{file_name}_channel"\n) : %(import "./#{file_name}_channel"\n) + end + + def install_javascript_dependencies + say "Installing JavaScript dependencies", :green + if using_bun? + run "bun add @rails/actioncable" + elsif using_node? + run "yarn add @rails/actioncable" + end + end + + def pin_javascript_dependencies + append_to_file "config/importmap.rb", <<-RUBY +pin "@rails/actioncable", to: "actioncable.esm.js" +pin_all_from "app/javascript/channels", under: "channels" + RUBY + end + def file_name @_file_name ||= super.sub(/_channel\z/i, "") end - # FIXME: Change these files to symlinks once RubyGems 2.5.0 is required. - def generate_application_cable_files - return if behavior != :invoke + def first_setup_required? + !root.join("app/javascript/channels/index.js").exist? + end - files = [ - "application_cable/channel.rb", - "application_cable/connection.rb" - ] + def using_javascript? + @using_javascript ||= options[:assets] && root.join("app/javascript").exist? + end - files.each do |name| - path = File.join("app/channels/", name) - template(name, path) if !File.exist?(path) - end + def using_js_runtime? + @using_js_runtime ||= root.join("package.json").exist? + end + + def using_bun? + # Cannot assume Bun lockfile has been generated yet so we look for a file known to + # be generated by the jsbundling-rails gem + @using_bun ||= using_js_runtime? && root.join("bun.config.js").exist? + end + + def using_node? + # Bun is the only runtime that _isn't_ node. + @using_node ||= using_js_runtime? && !root.join("bun.config.js").exist? + end + + def using_importmap? + @using_importmap ||= root.join("config/importmap.rb").exist? + end + + def root + @root ||= Pathname(destination_root) end end end diff --git a/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt index 0cfcf7491966d..08dc8af2a03bd 100644 --- a/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt +++ b/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt @@ -1,5 +1 @@ -// Load all the channels within this directory and all subdirectories. -// Channel files must be named *_channel.js. - -const channels = require.context('.', true, /_channel\.js$/) -channels.keys().forEach(channels) +// Import all the channels to be used by Action Cable diff --git a/actioncable/lib/rails/generators/test_unit/channel_generator.rb b/actioncable/lib/rails/generators/test_unit/channel_generator.rb index 7d13a12f0a3d7..6054a3be77aff 100644 --- a/actioncable/lib/rails/generators/test_unit/channel_generator.rb +++ b/actioncable/lib/rails/generators/test_unit/channel_generator.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module TestUnit module Generators class ChannelGenerator < ::Rails::Generators::NamedBase diff --git a/actioncable/package.json b/actioncable/package.json index a02813696301e..0a6c0d20b2830 100644 --- a/actioncable/package.json +++ b/actioncable/package.json @@ -1,8 +1,9 @@ { "name": "@rails/actioncable", - "version": "6.1.0-alpha", + "version": "8.1.0-alpha", "description": "WebSocket framework for Ruby on Rails.", - "main": "app/assets/javascripts/action_cable.js", + "module": "app/assets/javascripts/actioncable.esm.js", + "main": "app/assets/javascripts/actioncable.js", "files": [ "app/assets/javascripts/*.js", "src/*.js" @@ -23,22 +24,18 @@ }, "homepage": "https://rubyonrails.org/", "devDependencies": { - "babel-core": "^6.25.0", - "babel-plugin-external-helpers": "^6.22.0", - "babel-preset-env": "^1.6.0", - "eslint": "^4.3.0", - "eslint-plugin-import": "^2.7.0", - "karma": "^3.1.1", + "@rollup/plugin-commonjs": "^19.0.1", + "@rollup/plugin-node-resolve": "^11.0.1", + "eslint": "^8.40.0", + "eslint-plugin-import": "^2.29.0", + "karma": "^6.4.2", "karma-chrome-launcher": "^2.2.0", "karma-qunit": "^2.1.0", "karma-sauce-launcher": "^1.2.0", "mock-socket": "^2.0.0", "qunit": "^2.8.0", - "rollup": "^0.58.2", - "rollup-plugin-babel": "^3.0.4", - "rollup-plugin-commonjs": "^9.1.0", - "rollup-plugin-node-resolve": "^3.3.0", - "rollup-plugin-uglify": "^3.0.0" + "rollup": "^2.35.1", + "rollup-plugin-terser": "^7.0.2" }, "scripts": { "prebuild": "yarn lint && bundle exec rake assets:codegen", diff --git a/actioncable/rollup.config.js b/actioncable/rollup.config.js index 64727e0887d83..0ae1181be17dd 100644 --- a/actioncable/rollup.config.js +++ b/actioncable/rollup.config.js @@ -1,24 +1,46 @@ -import babel from "rollup-plugin-babel" -import uglify from "rollup-plugin-uglify" +import resolve from "@rollup/plugin-node-resolve" +import { terser } from "rollup-plugin-terser" -const uglifyOptions = { +const terserOptions = { mangle: false, compress: false, - output: { + format: { beautify: true, indent_level: 2 } } -export default { - input: "app/javascript/action_cable/index.js", - output: { - file: "app/assets/javascripts/action_cable.js", - format: "umd", - name: "ActionCable" +export default [ + { + input: "app/javascript/action_cable/index.js", + output: [ + { + file: "app/assets/javascripts/actioncable.js", + format: "umd", + name: "ActionCable" + }, + + { + file: "app/assets/javascripts/actioncable.esm.js", + format: "es" + } + ], + plugins: [ + resolve(), + terser(terserOptions) + ] }, - plugins: [ - babel(), - uglify(uglifyOptions) - ] -} + + { + input: "app/javascript/action_cable/index_with_name_deprecation.js", + output: { + file: "app/assets/javascripts/action_cable.js", + format: "umd", + name: "ActionCable" + }, + plugins: [ + resolve(), + terser(terserOptions) + ] + }, +] diff --git a/actioncable/rollup.config.test.js b/actioncable/rollup.config.test.js index f92ff36240711..06ddc8871ce52 100644 --- a/actioncable/rollup.config.test.js +++ b/actioncable/rollup.config.test.js @@ -1,6 +1,5 @@ -import babel from "rollup-plugin-babel" -import commonjs from "rollup-plugin-commonjs" -import resolve from "rollup-plugin-node-resolve" +import commonjs from "@rollup/plugin-commonjs" +import resolve from "@rollup/plugin-node-resolve" export default { input: "test/javascript/src/test.js", @@ -12,7 +11,6 @@ export default { plugins: [ resolve(), - commonjs(), - babel() + commonjs() ] } diff --git a/actioncable/test/channel/base_test.rb b/actioncable/test/channel/base_test.rb index 83b72e4f58b73..62e6958fcd8d0 100644 --- a/actioncable/test/channel/base_test.rb +++ b/actioncable/test/channel/base_test.rb @@ -105,6 +105,15 @@ def error_handler assert_equal({ id: 1 }, @channel.params) end + test "does not log filtered parameters" do + @connection.server.config.filter_parameters << :password + data = { password: "password", foo: "foo" } + + assert_logged({ password: "[FILTERED]" }.inspect[1..-2]) do + @channel.perform_action data + end + end + test "unsubscribing from a channel" do @channel.subscribe_to_channel @@ -117,6 +126,16 @@ def error_handler assert_not_predicate @channel, :subscribed? end + test "unsubscribed? method returns correct status" do + assert_not @channel.unsubscribed? + + @channel.subscribe_to_channel + assert_not @channel.unsubscribed? + + @channel.unsubscribe_from_channel + assert @channel.unsubscribed? + end + test "connection identifiers" do assert_equal @user.name, @channel.current_user.name end @@ -190,73 +209,49 @@ def error_handler end test "notification for perform_action" do - events = [] - ActiveSupport::Notifications.subscribe "perform_action.action_cable" do |*args| - events << ActiveSupport::Notifications::Event.new(*args) - end - data = { "action" => :speak, "content" => "hello" } - @channel.perform_action data + expected_payload = { channel_class: "ActionCable::Channel::BaseTest::ChatChannel", action: :speak, data: } - assert_equal 1, events.length - assert_equal "perform_action.action_cable", events[0].name - assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class] - assert_equal :speak, events[0].payload[:action] - assert_equal data, events[0].payload[:data] - ensure - ActiveSupport::Notifications.unsubscribe "perform_action.action_cable" + assert_notifications_count("perform_action.action_cable", 1) do + assert_notification("perform_action.action_cable", expected_payload) do + @channel.perform_action data + end + end end test "notification for transmit" do - events = [] - ActiveSupport::Notifications.subscribe "transmit.action_cable" do |*args| - events << ActiveSupport::Notifications::Event.new(*args) - end + data = { data: "latest" } + expected_payload = { channel_class: "ActionCable::Channel::BaseTest::ChatChannel", data:, via: nil } - @channel.perform_action "action" => :get_latest - expected_data = { data: "latest" } - - assert_equal 1, events.length - assert_equal "transmit.action_cable", events[0].name - assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class] - assert_equal expected_data, events[0].payload[:data] - assert_nil events[0].payload[:via] - ensure - ActiveSupport::Notifications.unsubscribe "transmit.action_cable" + assert_notifications_count("transmit.action_cable", 1) do + assert_notification("transmit.action_cable", expected_payload) do + @channel.perform_action "action" => :get_latest + end + end end test "notification for transmit_subscription_confirmation" do - @channel.subscribe_to_channel - - events = [] - ActiveSupport::Notifications.subscribe "transmit_subscription_confirmation.action_cable" do |*args| - events << ActiveSupport::Notifications::Event.new(*args) - end + expected_payload = { channel_class: "ActionCable::Channel::BaseTest::ChatChannel", identifier: "{id: 1}" } - @channel.stub(:subscription_confirmation_sent?, false) do - @channel.send(:transmit_subscription_confirmation) + @channel.subscribe_to_channel - assert_equal 1, events.length - assert_equal "transmit_subscription_confirmation.action_cable", events[0].name - assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class] + assert_notifications_count("transmit_subscription_confirmation.action_cable", 1) do + assert_notification("transmit_subscription_confirmation.action_cable", expected_payload) do + @channel.stub(:subscription_confirmation_sent?, false) do + @channel.send(:transmit_subscription_confirmation) + end + end end - ensure - ActiveSupport::Notifications.unsubscribe "transmit_subscription_confirmation.action_cable" end test "notification for transmit_subscription_rejection" do - events = [] - ActiveSupport::Notifications.subscribe "transmit_subscription_rejection.action_cable" do |*args| - events << ActiveSupport::Notifications::Event.new(*args) - end + expected_payload = { channel_class: "ActionCable::Channel::BaseTest::ChatChannel", identifier: "{id: 1}" } - @channel.send(:transmit_subscription_rejection) - - assert_equal 1, events.length - assert_equal "transmit_subscription_rejection.action_cable", events[0].name - assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class] - ensure - ActiveSupport::Notifications.unsubscribe "transmit_subscription_rejection.action_cable" + assert_notifications_count("transmit_subscription_rejection.action_cable", 1) do + assert_notification("transmit_subscription_rejection.action_cable", expected_payload) do + @channel.send(:transmit_subscription_rejection) + end + end end test "behaves like rescuable" do diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb index 7d04ceb436f33..3aa42297d68ff 100644 --- a/actioncable/test/channel/stream_test.rb +++ b/actioncable/test/channel/stream_test.rb @@ -4,6 +4,7 @@ require "minitest/mock" require "stubs/test_connection" require "stubs/room" +require "concurrent/atomic/cyclic_barrier" module ActionCable::StreamTests class Connection < ActionCable::Connection::Base @@ -280,6 +281,63 @@ class StreamTest < ActionCable::TestCase end end + test "concurrent unsubscribe_from_channel and stream_from do not raise RuntimeError" do + threads = [] + run_in_eventmachine do + connection = TestConnection.new + connection.pubsub.unsubscribe_latency = 0.1 + + channel = ChatChannel.new connection, "{id: 1}", id: 1 + channel.subscribe_to_channel + + # Set up initial streams + channel.stream_from "room_one" + channel.stream_from "room_two" + wait_for_async + + # Create barriers to synchronize thread execution + barrier = Concurrent::CyclicBarrier.new(2) + + exception_caught = nil + + # Thread 1: calls unsubscribe_from_channel + thread1 = Thread.new do + barrier.wait + # Add a small delay to increase the chance of concurrent execution + sleep 0.001 + channel.unsubscribe_from_channel + rescue => e + exception_caught = e + ensure + barrier.wait + end + threads << thread1 + + # Thread 2: calls stream_from during unsubscribe_from_channel iteration + thread2 = Thread.new do + barrier.wait + # Try to add streams while unsubscribe_from_channel is potentially iterating + 10.times do |i| + channel.stream_from "concurrent_room_#{i}" + sleep 0.0001 # Small delay to interleave with unsubscribe_from_channel + end + rescue => e + exception_caught = e + ensure + barrier.wait + end + threads << thread2 + + thread1.join + thread2.join + + # Ensure no RuntimeError was raised during concurrent access + assert_nil exception_caught, "Concurrent unsubscribe_from_channel and stream_from should not raise RuntimeError: #{exception_caught}" + end + ensure + threads.each(&:kill) + end + private def subscribers_of(connection) connection @@ -288,8 +346,6 @@ def subscribers_of(connection) end end - require "action_cable/subscription_adapter/async" - class UserCallbackChannel < ActionCable::Channel::Base def subscribed stream_from :channel do diff --git a/actioncable/test/channel/test_case_test.rb b/actioncable/test/channel/test_case_test.rb index 33b4d3b300f02..48ef86998bf54 100644 --- a/actioncable/test/channel/test_case_test.rb +++ b/actioncable/test/channel/test_case_test.rb @@ -44,7 +44,7 @@ def test_no_subscribe def test_subscribe subscribe - assert subscription.confirmed? + assert_predicate subscription, :confirmed? assert_not subscription.rejected? assert_equal 1, connection.transmissions.size assert_equal ActionCable::INTERNAL[:message_types][:confirmation], @@ -62,6 +62,7 @@ def test_connection_identifiers assert_equal "John", subscription.username assert subscription.admin + assert_equal "John:true", connection.connection_identifier end end @@ -76,7 +77,7 @@ def test_rejection subscribe assert_not subscription.confirmed? - assert subscription.rejected? + assert_predicate subscription, :rejected? assert_equal 1, connection.transmissions.size assert_equal ActionCable::INTERNAL[:message_types][:rejection], connection.transmissions.last["type"] @@ -87,6 +88,10 @@ class StreamsTestChannel < ActionCable::Channel::Base def subscribed stream_from "test_#{params[:id] || 0}" end + + def unsubscribed + stop_stream_from "test_#{params[:id] || 0}" + end end class StreamsTestChannelTest < ActionCable::Channel::TestCase @@ -101,12 +106,37 @@ def test_stream_with_params assert_has_stream "test_42" end + + def test_not_stream_without_params + subscribe + unsubscribe + + assert_has_no_stream "test_0" + end + + def test_not_stream_with_params + subscribe id: 42 + perform :unsubscribed, id: 42 + + assert_has_no_stream "test_42" + end + + def test_unsubscribe_from_stream + subscribe + unsubscribe + + assert_no_streams + end end class StreamsForTestChannel < ActionCable::Channel::Base def subscribed stream_for User.new(params[:id]) end + + def unsubscribed + stop_stream_for User.new(params[:id]) + end end class StreamsForTestChannelTest < ActionCable::Channel::TestCase @@ -115,6 +145,13 @@ def test_stream_with_params assert_has_stream_for User.new(42) end + + def test_not_stream_with_params + subscribe id: 42 + perform :unsubscribed, id: 42 + + assert_has_no_stream_for User.new(42) + end end class NoStreamsTestChannel < ActionCable::Channel::Base diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb index 5866ca7733cbf..9069510c89311 100644 --- a/actioncable/test/client_test.rb +++ b/actioncable/test/client_test.rb @@ -8,28 +8,18 @@ require "active_support/hash_with_indifferent_access" -#### -# 😷 Warning suppression 😷 -WebSocket::Frame::Handler::Handler03.prepend Module.new { - def initialize(*) - @application_data_buffer = nil - super - end -} - -WebSocket::Frame::Data.prepend Module.new { - def initialize(*) - @masking_key = nil - super - end -} -# -#### - class ClientTest < ActionCable::TestCase WAIT_WHEN_EXPECTING_EVENT = 2 WAIT_WHEN_NOT_EXPECTING_EVENT = 0.5 + class Connection < ActionCable::Connection::Base + identified_by :id + + def connect + self.id = request.params["id"] || SecureRandom.hex(4) + end + end + class EchoChannel < ActionCable::Channel::Base def subscribed stream_from "global" @@ -59,16 +49,22 @@ def setup server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN } server.config.cable = ActiveSupport::HashWithIndifferentAccess.new(adapter: "async") + server.config.connection_class = -> { ClientTest::Connection } # and now the "real" setup for our test: server.config.disable_request_forgery_protection = true end def with_puma_server(rack_app = ActionCable.server, port = 3099) - server = ::Puma::Server.new(rack_app, ::Puma::Events.strings) + opts = { min_threads: 1, max_threads: 4 } + server = if Puma::Const::PUMA_VERSION >= "6" + opts[:log_writer] = ::Puma::LogWriter.strings + ::Puma::Server.new(rack_app, nil, opts) + else + # Puma >= 5.0.3 + ::Puma::Server.new(rack_app, ::Puma::Events.strings, opts) + end server.add_tcp_listener "127.0.0.1", port - server.min_threads = 1 - server.max_threads = 4 thread = server.run @@ -102,7 +98,7 @@ def with_puma_server(rack_app = ActionCable.server, port = 3099) class SyncClient attr_reader :pings - def initialize(port) + def initialize(port, path = "/") messages = @messages = Queue.new closed = @closed = Concurrent::Event.new has_messages = @has_messages = Concurrent::Semaphore.new(0) @@ -110,7 +106,7 @@ def initialize(port) open = Concurrent::Promise.new - @ws = WebSocket::Client::Simple.connect("ws://127.0.0.1:#{port}/") do |ws| + @ws = WebSocket::Client::Simple.connect("ws://127.0.0.1:#{port}#{path}") do |ws| ws.on(:error) do |event| event = RuntimeError.new(event.message) unless event.is_a?(Exception) @@ -196,12 +192,12 @@ def closed? end end - def websocket_client(port) - SyncClient.new(port) + def websocket_client(*args) + SyncClient.new(*args) end def concurrently(enum) - enum.map { |*x| Concurrent::Future.execute { yield(*x) } }.map(&:value!) + enum.map { |*x| Concurrent::Promises.future { yield(*x) } }.map(&:value!) end def test_single_client @@ -284,7 +280,6 @@ def test_unsubscribe_client c.send_message command: "subscribe", identifier: identifier assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message) assert_equal(1, app.connections.count) - assert(app.remote_connections.where(identifier: identifier)) subscriptions = app.connections.first.subscriptions.send(:subscriptions) assert_not_equal 0, subscriptions.size, "Missing EchoChannel subscription" @@ -299,6 +294,40 @@ def test_unsubscribe_client end end + def test_remote_disconnect_client + with_puma_server do |port| + app = ActionCable.server + + c = websocket_client(port, "/?id=1") + assert_equal({ "type" => "welcome" }, c.read_message) + + sleep 0.1 # make sure connections is registered + app.remote_connections.where(id: "1").disconnect + + assert_equal({ "type" => "disconnect", "reason" => "remote", "reconnect" => true }, c.read_message) + + c.wait_for_close + assert_predicate(c, :closed?) + end + end + + def test_remote_disconnect_client_with_reconnect + with_puma_server do |port| + app = ActionCable.server + + c = websocket_client(port, "/?id=2") + assert_equal({ "type" => "welcome" }, c.read_message) + + sleep 0.1 # make sure connections is registered + app.remote_connections.where(id: "2").disconnect(reconnect: false) + + assert_equal({ "type" => "disconnect", "reason" => "remote", "reconnect" => false }, c.read_message) + + c.wait_for_close + assert_predicate(c, :closed?) + end + end + def test_server_restart with_puma_server do |port| c = websocket_client(port) diff --git a/actioncable/test/connection/callbacks_test.rb b/actioncable/test/connection/callbacks_test.rb new file mode 100644 index 0000000000000..0552a9ebf1d3e --- /dev/null +++ b/actioncable/test/connection/callbacks_test.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require "test_helper" +require "stubs/test_server" + +class ActionCable::Connection::CallbacksTest < ActionCable::TestCase + class Connection < ActionCable::Connection::Base + identified_by :context + + attr_reader :commands_counter + + before_command do + throw :abort unless context.nil? + end + + around_command :set_current_context + after_command :increment_commands_counter + + def initialize(*) + super + @commands_counter = 0 + end + + private + def set_current_context + self.context = request.params["context"] + yield + ensure + self.context = nil + end + + def increment_commands_counter + @commands_counter += 1 + end + end + + class ChatChannel < ActionCable::Channel::Base + class << self + attr_accessor :words_spoken, :subscribed_count + end + + self.words_spoken = [] + self.subscribed_count = 0 + + def subscribed + self.class.subscribed_count += 1 + end + + def speak(data) + self.class.words_spoken << { data: data, context: context } + end + end + + setup do + @server = TestServer.new + @env = Rack::MockRequest.env_for "/test", "HTTP_HOST" => "localhost", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket" + @connection = Connection.new(@server, @env) + @identifier = { channel: "ActionCable::Connection::CallbacksTest::ChatChannel" }.to_json + end + + attr_reader :server, :env, :connection, :identifier + + test "before and after callbacks" do + result = assert_difference -> { ChatChannel.subscribed_count }, +1 do + assert_difference -> { connection.commands_counter }, +1 do + connection.handle_channel_command({ "identifier" => identifier, "command" => "subscribe" }) + end + end + assert result + end + + test "before callback halts" do + connection.context = "non_null" + result = assert_no_difference -> { ChatChannel.subscribed_count } do + connection.handle_channel_command({ "identifier" => identifier, "command" => "subscribe" }) + end + assert_not result + end + + test "around_command callback" do + env["QUERY_STRING"] = "context=test" + connection = Connection.new(server, env) + + assert_difference -> { ChatChannel.words_spoken.size }, +1 do + # We need to add subscriptions first + connection.handle_channel_command({ + "identifier" => identifier, + "command" => "subscribe" + }) + connection.handle_channel_command({ + "identifier" => identifier, + "command" => "message", + "data" => { "action" => "speak", "message" => "hello" }.to_json + }) + end + + message = ChatChannel.words_spoken.last + assert_equal({ data: { "action" => "speak", "message" => "hello" }, context: "test" }, message) + end +end diff --git a/actioncable/test/connection/stream_test.rb b/actioncable/test/connection/stream_test.rb index 0f4576db401f3..c579f1cd683c7 100644 --- a/actioncable/test/connection/stream_test.rb +++ b/actioncable/test/connection/stream_test.rb @@ -38,12 +38,12 @@ def on_error(message) [ EOFError, Errno::ECONNRESET ].each do |closed_exception| test "closes socket on #{closed_exception}" do run_in_eventmachine do - connection = open_connection + rack_hijack_io = File.open(File::NULL, "w") + connection = open_connection(rack_hijack_io) # Internal hax = :( client = connection.websocket.send(:websocket) - rack_hijack_io = client.instance_variable_get("@stream").instance_variable_get("@rack_hijack_io") - rack_hijack_io.stub(:write, proc { raise(closed_exception, "foo") }) do + rack_hijack_io.stub(:write_nonblock, proc { raise(closed_exception, "foo") }) do assert_called(client, :client_gone) do client.write("boo") end @@ -54,11 +54,11 @@ def on_error(message) end private - def open_connection + def open_connection(io) env = Rack::MockRequest.env_for "/test", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket", "HTTP_HOST" => "localhost", "HTTP_ORIGIN" => "http://rubyonrails.com" - env["rack.hijack"] = -> { env["rack.hijack_io"] = StringIO.new } + env["rack.hijack"] = -> { env["rack.hijack_io"] = io } Connection.new(@server, env).tap do |connection| connection.process diff --git a/actioncable/test/connection/subscriptions_test.rb b/actioncable/test/connection/subscriptions_test.rb index 1da2f781792ed..0af3d1be2f9f7 100644 --- a/actioncable/test/connection/subscriptions_test.rb +++ b/actioncable/test/connection/subscriptions_test.rb @@ -66,6 +66,17 @@ def throw_exception(_data) end end + test "subscribe command with Base channel" do + run_in_eventmachine do + setup_connection + + identifier = ActiveSupport::JSON.encode(id: 1, channel: "ActionCable::Channel::Base") + @subscriptions.execute_command "command" => "subscribe", "identifier" => identifier + + assert_empty @subscriptions.identifiers + end + end + test "unsubscribe command" do run_in_eventmachine do setup_connection diff --git a/actioncable/test/connection/test_case_test.rb b/actioncable/test/connection/test_case_test.rb index 3b19465d7b868..7852fdeca7d01 100644 --- a/actioncable/test/connection/test_case_test.rb +++ b/actioncable/test/connection/test_case_test.rb @@ -47,6 +47,14 @@ def test_plain_cookie assert_equal "456", connection.user_id end + def test_plain_cookie_with_explicit_value_and_string_key + cookies["user_id"] = { "value" => "456" } + + connect + + assert_equal "456", connection.user_id + end + def test_disconnect cookies["user_id"] = "456" @@ -133,6 +141,14 @@ def test_connected_with_encrypted_cookies assert_equal "456", connection.user_id end + def test_connected_with_encrypted_cookies_with_explicit_value_and_symbol_key + cookies.encrypted["user_id"] = { value: "456" } + + connect + + assert_equal "456", connection.user_id + end + def test_connection_rejected assert_reject_connection { connect } end diff --git a/actioncable/test/javascript/src/test.js b/actioncable/test/javascript/src/test.js index eea1c0a408bcb..938f71a2fa3a2 100644 --- a/actioncable/test/javascript/src/test.js +++ b/actioncable/test/javascript/src/test.js @@ -1,6 +1,8 @@ import "./test_helpers/index" import "./unit/action_cable_test" import "./unit/connection_test" +import "./unit/connection_monitor_test" import "./unit/consumer_test" import "./unit/subscription_test" import "./unit/subscriptions_test" +import "./unit/subscription_guarantor_test" diff --git a/actioncable/test/javascript/src/test_helpers/consumer_test_helper.js b/actioncable/test/javascript/src/test_helpers/consumer_test_helper.js index d1dabc9fc40fb..30e8c277bc999 100644 --- a/actioncable/test/javascript/src/test_helpers/consumer_test_helper.js +++ b/actioncable/test/javascript/src/test_helpers/consumer_test_helper.js @@ -17,6 +17,10 @@ export default function(name, options, callback) { ActionCable.adapters.WebSocket = MockWebSocket const server = new MockServer(options.url) const consumer = ActionCable.createConsumer(options.url) + const connection = consumer.connection + const monitor = connection.monitor + + if ("subprotocols" in options) consumer.addSubProtocol(options.subprotocols) server.on("connection", function() { const clients = server.clients() @@ -43,7 +47,7 @@ export default function(name, options, callback) { doneAsync() } - const testData = {assert, consumer, server, done} + const testData = {assert, consumer, connection, monitor, server, done} if (options.connect === false) { callback(testData) diff --git a/actioncable/test/javascript/src/unit/action_cable_test.js b/actioncable/test/javascript/src/unit/action_cable_test.js index c46f9878d2a94..017959ab8ca80 100644 --- a/actioncable/test/javascript/src/unit/action_cable_test.js +++ b/actioncable/test/javascript/src/unit/action_cable_test.js @@ -6,13 +6,13 @@ const {module, test} = QUnit module("ActionCable", () => { module("Adapters", () => { module("WebSocket", () => { - test("default is self.WebSocket", assert => { + test("default is WebSocket", assert => { assert.equal(ActionCable.adapters.WebSocket, self.WebSocket) }) }) module("logger", () => { - test("default is self.console", assert => { + test("default is console", assert => { assert.equal(ActionCable.adapters.logger, self.console) }) }) diff --git a/actioncable/test/javascript/src/unit/connection_monitor_test.js b/actioncable/test/javascript/src/unit/connection_monitor_test.js new file mode 100644 index 0000000000000..ac5d92494ec99 --- /dev/null +++ b/actioncable/test/javascript/src/unit/connection_monitor_test.js @@ -0,0 +1,68 @@ +import * as ActionCable from "../../../../app/javascript/action_cable/index" + +const {module, test} = QUnit + +module("ActionCable.ConnectionMonitor", hooks => { + let monitor + hooks.beforeEach(() => monitor = new ActionCable.ConnectionMonitor({})) + + module("#getPollInterval", hooks => { + hooks.beforeEach(() => Math._random = Math.random) + hooks.afterEach(() => Math.random = Math._random) + + const { staleThreshold, reconnectionBackoffRate } = ActionCable.ConnectionMonitor + const backoffFactor = 1 + reconnectionBackoffRate + const ms = 1000 + + test("uses exponential backoff", assert => { + Math.random = () => 0 + + monitor.reconnectAttempts = 0 + assert.equal(monitor.getPollInterval(), staleThreshold * ms) + + monitor.reconnectAttempts = 1 + assert.equal(monitor.getPollInterval(), staleThreshold * backoffFactor * ms) + + monitor.reconnectAttempts = 2 + assert.equal(monitor.getPollInterval(), staleThreshold * backoffFactor * backoffFactor * ms) + }) + + test("caps exponential backoff after some number of reconnection attempts", assert => { + Math.random = () => 0 + monitor.reconnectAttempts = 42 + const cappedPollInterval = monitor.getPollInterval() + + monitor.reconnectAttempts = 9001 + assert.equal(monitor.getPollInterval(), cappedPollInterval) + }) + + test("uses 100% jitter when 0 reconnection attempts", assert => { + Math.random = () => 0 + assert.equal(monitor.getPollInterval(), staleThreshold * ms) + + Math.random = () => 0.5 + assert.equal(monitor.getPollInterval(), staleThreshold * 1.5 * ms) + }) + + test("uses reconnectionBackoffRate for jitter when >0 reconnection attempts", assert => { + monitor.reconnectAttempts = 1 + + Math.random = () => 0.25 + assert.equal(monitor.getPollInterval(), staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.25) * ms) + + Math.random = () => 0.5 + assert.equal(monitor.getPollInterval(), staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.5) * ms) + }) + + test("applies jitter after capped exponential backoff", assert => { + monitor.reconnectAttempts = 9001 + + Math.random = () => 0 + const withoutJitter = monitor.getPollInterval() + Math.random = () => 0.5 + const withJitter = monitor.getPollInterval() + + assert.ok(withJitter > withoutJitter) + }) + }) +}) diff --git a/actioncable/test/javascript/src/unit/consumer_test.js b/actioncable/test/javascript/src/unit/consumer_test.js index acc618bf0ce7e..1eba5bbb4471f 100644 --- a/actioncable/test/javascript/src/unit/consumer_test.js +++ b/actioncable/test/javascript/src/unit/consumer_test.js @@ -16,4 +16,12 @@ module("ActionCable.Consumer", () => { client.addEventListener("close", done) consumer.disconnect() }) + + consumerTest("#addSubProtocol", {subprotocols: "some subprotocol"}, ({consumer, server, assert, done}) => { + server.on("connection", () => { + assert.equal(consumer.subprotocols.length, 1) + assert.equal(consumer.subprotocols[0], "some subprotocol") + done() + }) + }) }) diff --git a/actioncable/test/javascript/src/unit/subscription_guarantor_test.js b/actioncable/test/javascript/src/unit/subscription_guarantor_test.js new file mode 100644 index 0000000000000..83665344f5d67 --- /dev/null +++ b/actioncable/test/javascript/src/unit/subscription_guarantor_test.js @@ -0,0 +1,32 @@ +import * as ActionCable from "../../../../app/javascript/action_cable/index" + +const {module, test} = QUnit + +module("ActionCable.SubscriptionGuarantor", hooks => { + let guarantor + hooks.beforeEach(() => guarantor = new ActionCable.SubscriptionGuarantor({})) + + module("#guarantee", () => { + test("guarantees subscription only once", assert => { + const sub = {} + + assert.equal(guarantor.pendingSubscriptions.length, 0) + guarantor.guarantee(sub) + assert.equal(guarantor.pendingSubscriptions.length, 1) + guarantor.guarantee(sub) + assert.equal(guarantor.pendingSubscriptions.length, 1) + }) + }), + + module("#forget", () => { + test("removes subscription", assert => { + const sub = {} + + assert.equal(guarantor.pendingSubscriptions.length, 0) + guarantor.guarantee(sub) + assert.equal(guarantor.pendingSubscriptions.length, 1) + guarantor.forget(sub) + assert.equal(guarantor.pendingSubscriptions.length, 0) + }) + }) +}) diff --git a/actioncable/test/javascript/src/unit/subscription_test.js b/actioncable/test/javascript/src/unit/subscription_test.js index bf32e5f27d979..f1c2efabad33e 100644 --- a/actioncable/test/javascript/src/unit/subscription_test.js +++ b/actioncable/test/javascript/src/unit/subscription_test.js @@ -14,12 +14,26 @@ module("ActionCable.Subscription", () => { consumerTest("#connected callback", ({server, consumer, assert, done}) => { const subscription = consumer.subscriptions.create("chat", { - connected() { + connected({reconnected}) { assert.ok(true) + assert.notOk(reconnected) + done() + } + }) + + server.broadcastTo(subscription, {message_type: "confirmation"}) + }) + + consumerTest("#connected callback (handling reconnects)", ({server, consumer, connection, monitor, assert, done}) => { + const subscription = consumer.subscriptions.create("chat", { + connected({reconnected}) { + assert.ok(reconnected) done() } }) + monitor.reconnectAttempts = 1 + server.broadcastTo(subscription, {message_type: "welcome"}) server.broadcastTo(subscription, {message_type: "confirmation"}) }) diff --git a/actioncable/test/javascript_package_test.rb b/actioncable/test/javascript_package_test.rb index 421eab2d60c77..948f98e68698f 100644 --- a/actioncable/test/javascript_package_test.rb +++ b/actioncable/test/javascript_package_test.rb @@ -4,10 +4,16 @@ class JavascriptPackageTest < ActiveSupport::TestCase def test_compiled_code_is_in_sync_with_source_code - compiled_file = File.expand_path("../app/assets/javascripts/action_cable.js", __dir__) + compiled_files = %w[ + app/assets/javascripts/actioncable.js + app/assets/javascripts/actioncable.esm.js + app/assets/javascripts/action_cable.js + ].map do |file| + Pathname(file).expand_path("#{__dir__}/..") + end - assert_no_changes -> { File.read(compiled_file) } do - system "yarn build" + assert_no_changes -> { compiled_files.map(&:read) } do + system "yarn build", exception: true end end end diff --git a/actioncable/test/server/broadcasting_test.rb b/actioncable/test/server/broadcasting_test.rb index 860e79b821d99..9caa9db477dd5 100644 --- a/actioncable/test/server/broadcasting_test.rb +++ b/actioncable/test/server/broadcasting_test.rb @@ -4,55 +4,37 @@ require "stubs/test_server" class BroadcastingTest < ActionCable::TestCase - test "fetching a broadcaster converts the broadcasting queue to a string" do - broadcasting = :test_queue - server = TestServer.new - broadcaster = server.broadcaster_for(broadcasting) + setup do + @server = TestServer.new + @broadcasting = "test_queue" + @broadcaster = server.broadcaster_for(@broadcasting) + end + + attr_reader :server, :broadcasting, :broadcaster + test "fetching a broadcaster converts the broadcasting queue to a string" do assert_equal "test_queue", broadcaster.broadcasting end test "broadcast generates notification" do - server = TestServer.new + message = { body: "test message" } + expected_payload = { broadcasting:, message:, coder: ActiveSupport::JSON } - events = [] - ActiveSupport::Notifications.subscribe "broadcast.action_cable" do |*args| - events << ActiveSupport::Notifications::Event.new(*args) + assert_notifications_count("broadcast.action_cable", 1) do + assert_notification("broadcast.action_cable", expected_payload) do + server.broadcast(broadcasting, message) + end end - - broadcasting = "test_queue" - message = { body: "test message" } - server.broadcast(broadcasting, message) - - assert_equal 1, events.length - assert_equal "broadcast.action_cable", events[0].name - assert_equal broadcasting, events[0].payload[:broadcasting] - assert_equal message, events[0].payload[:message] - assert_equal ActiveSupport::JSON, events[0].payload[:coder] - ensure - ActiveSupport::Notifications.unsubscribe "broadcast.action_cable" end test "broadcaster from broadcaster_for generates notification" do - server = TestServer.new - - events = [] - ActiveSupport::Notifications.subscribe "broadcast.action_cable" do |*args| - events << ActiveSupport::Notifications::Event.new(*args) - end - - broadcasting = "test_queue" message = { body: "test message" } + expected_payload = { broadcasting:, message:, coder: ActiveSupport::JSON } - broadcaster = server.broadcaster_for(broadcasting) - broadcaster.broadcast(message) - - assert_equal 1, events.length - assert_equal "broadcast.action_cable", events[0].name - assert_equal broadcasting, events[0].payload[:broadcasting] - assert_equal message, events[0].payload[:message] - assert_equal ActiveSupport::JSON, events[0].payload[:coder] - ensure - ActiveSupport::Notifications.unsubscribe "broadcast.action_cable" + assert_notifications_count("broadcast.action_cable", 1) do + assert_notification("broadcast.action_cable", expected_payload) do + broadcaster.broadcast(message) + end + end end end diff --git a/actioncable/test/server/health_check_test.rb b/actioncable/test/server/health_check_test.rb new file mode 100644 index 0000000000000..4fa3a108fb192 --- /dev/null +++ b/actioncable/test/server/health_check_test.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "test_helper" +require "active_support/core_ext/hash/indifferent_access" + +class HealthCheckTest < ActionCable::TestCase + def setup + @config = ActionCable::Server::Configuration.new + @config.logger = Logger.new(nil) + @server = ActionCable::Server::Base.new config: @config + @server.config.cable = { adapter: "async" }.with_indifferent_access + + @app = Rack::Lint.new(@server) + end + + + test "no health check app are mounted by default" do + get "/up" + assert_equal 404, response.first + end + + test "setting health_check_path mount the configured health check application" do + @server.config.health_check_path = "/up" + get "/up" + + assert_equal 200, response.first + assert_equal [], response.last.enum_for.to_a + end + + test "health_check_application_can_be_customized" do + @server.config.health_check_path = "/up" + @server.config.health_check_application = health_check_application + get "/up" + + assert_equal 200, response.first + assert_equal ["Hello world!"], response.last.enum_for.to_a + end + + + private + def get(path) + env = Rack::MockRequest.env_for "/up", "HTTP_HOST" => "localhost" + @response = @app.call env + end + + attr_reader :response + + def health_check_application + ->(env) { + [ + 200, + { Rack::CONTENT_TYPE => "text/html" }, + ["Hello world!"], + ] + } + end +end diff --git a/actioncable/test/stubs/test_adapter.rb b/actioncable/test/stubs/test_adapter.rb index 22822acdffaa5..1ae5976fa0011 100644 --- a/actioncable/test/stubs/test_adapter.rb +++ b/actioncable/test/stubs/test_adapter.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true class SuccessAdapter < ActionCable::SubscriptionAdapter::Base + attr_accessor :unsubscribe_latency + + def initialize(...) + super + @unsubscribe_latency = nil + end + def broadcast(channel, payload) end @@ -10,6 +17,7 @@ def subscribe(channel, callback, success_callback = nil) end def unsubscribe(channel, callback) + sleep @unsubscribe_latency if @unsubscribe_latency subscriber_map[channel].delete(callback) subscriber_map.delete(channel) if subscriber_map[channel].empty? @@unsubscribe_called = { channel: channel, callback: callback } diff --git a/actioncable/test/stubs/test_connection.rb b/actioncable/test/stubs/test_connection.rb index 155c68e38ccf2..ca026516bb9f3 100644 --- a/actioncable/test/stubs/test_connection.rb +++ b/actioncable/test/stubs/test_connection.rb @@ -5,7 +5,7 @@ class TestConnection attr_reader :identifiers, :logger, :current_user, :server, :subscriptions, :transmissions - delegate :pubsub, to: :server + delegate :pubsub, :config, to: :server def initialize(user = User.new("lifo"), coder: ActiveSupport::JSON, subscription_adapter: SuccessAdapter) @coder = coder diff --git a/actioncable/test/stubs/test_server.rb b/actioncable/test/stubs/test_server.rb index 0bc4625e28fa0..2bdf0a28bed91 100644 --- a/actioncable/test/stubs/test_server.rb +++ b/actioncable/test/stubs/test_server.rb @@ -1,18 +1,28 @@ # frozen_string_literal: true -require "ostruct" - class TestServer include ActionCable::Server::Connections include ActionCable::Server::Broadcasting attr_reader :logger, :config, :mutex - def initialize(subscription_adapter: SuccessAdapter) - @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new) + class FakeConfiguration < ActionCable::Server::Configuration + attr_accessor :subscription_adapter, :log_tags, :filter_parameters - @config = OpenStruct.new(log_tags: [], subscription_adapter: subscription_adapter) + def initialize(subscription_adapter:) + @log_tags = [] + @filter_parameters = [] + @subscription_adapter = subscription_adapter + end + def pubsub_adapter + @subscription_adapter + end + end + + def initialize(subscription_adapter: SuccessAdapter) + @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new) + @config = FakeConfiguration.new(subscription_adapter: subscription_adapter) @mutex = Monitor.new end diff --git a/actioncable/test/subscription_adapter/channel_prefix.rb b/actioncable/test/subscription_adapter/channel_prefix.rb index 475e6cfd3ad5f..16baa4e2d6c3a 100644 --- a/actioncable/test/subscription_adapter/channel_prefix.rb +++ b/actioncable/test/subscription_adapter/channel_prefix.rb @@ -5,7 +5,7 @@ module ChannelPrefixTest def test_channel_prefix server2 = ActionCable::Server::Base.new(config: ActionCable::Server::Configuration.new) - server2.config.cable = alt_cable_config + server2.config.cable = alt_cable_config.with_indifferent_access server2.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN } adapter_klass = server2.config.pubsub_adapter diff --git a/actioncable/test/subscription_adapter/postgresql_test.rb b/actioncable/test/subscription_adapter/postgresql_test.rb index 186f37156142d..5dd091621e818 100644 --- a/actioncable/test/subscription_adapter/postgresql_test.rb +++ b/actioncable/test/subscription_adapter/postgresql_test.rb @@ -23,7 +23,7 @@ def setup ActiveRecord::Base.establish_connection database_config begin - ActiveRecord::Base.connection + ActiveRecord::Base.lease_connection.connect! rescue @rx_adapter = @tx_adapter = nil skip "Couldn't connect to PostgreSQL: #{database_config.inspect}" @@ -35,7 +35,7 @@ def setup def teardown super - ActiveRecord::Base.clear_all_connections! + ActiveRecord::Base.connection_handler.clear_all_connections! end def cable_config @@ -60,15 +60,15 @@ def active? assert_equal "hello world", queue.pop end - ActiveRecord::Base.clear_reloadable_connections! + ActiveRecord::Base.connection_handler.clear_reloadable_connections! - assert adapter.active? + assert_predicate adapter, :active? end def test_default_subscription_connection_identifier subscribe_as_queue("channel") { } - identifiers = ActiveRecord::Base.connection.exec_query("SELECT application_name FROM pg_stat_activity").rows + identifiers = ActiveRecord::Base.lease_connection.exec_query("SELECT application_name FROM pg_stat_activity").rows assert_includes identifiers, ["ActionCable-PID-#{$$}"] end @@ -81,7 +81,7 @@ def test_custom_subscription_connection_identifier subscribe_as_queue("channel", adapter) { } - identifiers = ActiveRecord::Base.connection.exec_query("SELECT application_name FROM pg_stat_activity").rows + identifiers = ActiveRecord::Base.lease_connection.exec_query("SELECT application_name FROM pg_stat_activity").rows assert_includes identifiers, ["hello-world-42"] end end diff --git a/actioncable/test/subscription_adapter/redis_test.rb b/actioncable/test/subscription_adapter/redis_test.rb index 4f1fc584b3711..e90e4129ce89e 100644 --- a/actioncable/test/subscription_adapter/redis_test.rb +++ b/actioncable/test/subscription_adapter/redis_test.rb @@ -4,32 +4,74 @@ require_relative "common" require_relative "channel_prefix" -require "action_cable/subscription_adapter/redis" - class RedisAdapterTest < ActionCable::TestCase include CommonSubscriptionAdapterTest include ChannelPrefixTest def cable_config { adapter: "redis", driver: "ruby" }.tap do |x| - if host = URI(ENV["REDIS_URL"] || "").hostname - x[:host] = host + if host = ENV["REDIS_URL"] + x[:url] = host end end end -end -class RedisAdapterTest::Hiredis < RedisAdapterTest - def cable_config - super.merge(driver: "hiredis") + def test_reconnections + subscribe_as_queue("channel") do |queue| + subscribe_as_queue("other channel") do |queue_2| + @tx_adapter.broadcast("channel", "hello world") + + assert_equal "hello world", queue.pop + + drop_pubsub_connections + wait_pubsub_connection(redis_conn, "channel") + + @tx_adapter.broadcast("channel", "hallo welt") + + assert_equal "hallo welt", queue.pop + + drop_pubsub_connections + wait_pubsub_connection(redis_conn, "channel") + wait_pubsub_connection(redis_conn, "other channel") + + @tx_adapter.broadcast("channel", "hola mundo") + @tx_adapter.broadcast("other channel", "other message") + + assert_equal "hola mundo", queue.pop + assert_equal "other message", queue_2.pop + end + end end + + private + def redis_conn + @redis_conn ||= ::Redis.new(cable_config.except(:adapter)) + end + + def drop_pubsub_connections + # Emulate connection failure by dropping all connections + redis_conn.client("kill", "type", "pubsub") + end + + def wait_pubsub_connection(redis_conn, channel, timeout: 5) + wait = timeout + loop do + break if redis_conn.pubsub("numsub", channel).last > 0 + + sleep 0.1 + wait -= 0.1 + + raise "Timed out to subscribe to #{channel}" if wait <= 0 + end + end end class RedisAdapterTest::AlternateConfiguration < RedisAdapterTest def cable_config alt_cable_config = super.dup alt_cable_config.delete(:url) - alt_cable_config.merge(host: URI(ENV["REDIS_URL"] || "").hostname || "127.0.0.1", port: 6379, db: 12) + url = URI(ENV["REDIS_URL"] || "") + alt_cable_config.merge(host: url.hostname || "127.0.0.1", port: url.port || 6379, db: 12) end end @@ -55,7 +97,7 @@ def expected_connection end test "sets connection id for connection" do - assert_called_with ::Redis, :new, [ expected_connection.stringify_keys ] do + assert_called_with ::Redis, :new, [ expected_connection.symbolize_keys ] do @adapter.send(:redis_connection) end end @@ -71,6 +113,16 @@ def connection_id end end +class RedisAdapterTest::ConnectorCustomIDNil < RedisAdapterTest::ConnectorDefaultID + def cable_config + super.merge(id: connection_id) + end + + def connection_id + nil + end +end + class RedisAdapterTest::ConnectorWithExcluded < RedisAdapterTest::ConnectorDefaultID def cable_config super.merge(adapter: "redis", channel_prefix: "custom") @@ -80,3 +132,31 @@ def expected_connection super.except(:adapter, :channel_prefix) end end + +class RedisAdapterTest::SentinelConfigAsHash < ActionCable::TestCase + def setup + server = ActionCable::Server::Base.new + server.config.cable = cable_config.merge(adapter: "redis").with_indifferent_access + server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN } + + @adapter = server.config.pubsub_adapter.new(server) + end + + def cable_config + { url: "redis://test", sentinels: [{ "host" => "localhost", "port" => 26379 }] } + end + + def expected_connection + { url: "redis://test", sentinels: [{ host: "localhost", port: 26379 }], id: connection_id } + end + + def connection_id + "ActionCable-PID-#{$$}" + end + + test "sets sentinels as array of hashes with keyword arguments" do + assert_called_with ::Redis, :new, [ expected_connection ] do + @adapter.send(:redis_connection) + end + end +end diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb index 033f034b0c654..d6305dcc766cf 100644 --- a/actioncable/test/test_helper.rb +++ b/actioncable/test/test_helper.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative "../../tools/strict_warnings" require "action_cable" require "active_support/testing/autorun" require "active_support/testing/method_call_assertions" @@ -7,11 +8,6 @@ require "puma" require "rack/mock" -begin - require "byebug" -rescue LoadError -end - # Require all the stubs and models Dir[File.expand_path("stubs/*.rb", __dir__)].each { |file| require file } diff --git a/actioncable/test/test_helper_test.rb b/actioncable/test/test_helper_test.rb index a771126b8f49d..ef5615f4b0b84 100644 --- a/actioncable/test/test_helper_test.rb +++ b/actioncable/test/test_helper_test.rb @@ -14,6 +14,21 @@ def test_assert_broadcasts end end + def test_capture_broadcasts + messages = capture_broadcasts("test") do + ActionCable.server.broadcast "test", "message" + end + assert_equal "message", messages.first + + messages = capture_broadcasts("test") do + ActionCable.server.broadcast "test", { message: "one" } + ActionCable.server.broadcast "test", { message: "two" } + end + assert_equal 2, messages.length + assert_equal({ "message" => "one" }, messages.first) + assert_equal({ "message" => "two" }, messages.last) + end + def test_assert_broadcasts_with_no_block assert_nothing_raised do ActionCable.server.broadcast "test", "message" @@ -112,5 +127,15 @@ def test_assert_broadcast_on_message end assert_match(/No messages sent/, error.message) + assert_match(/Message\(s\) found:\nhello/, error.message) + end + + def test_assert_broadcast_on_message_with_empty_channel + error = assert_raises Minitest::Assertion do + assert_broadcast_on("test", "world") + end + + assert_match(/No messages sent/, error.message) + assert_match(/No message found for test/, error.message) end end diff --git a/actionmailbox/.gitignore b/actionmailbox/.gitignore index 3ac26defd79ba..d84c713a08039 100644 --- a/actionmailbox/.gitignore +++ b/actionmailbox/.gitignore @@ -1,3 +1,5 @@ +/test/dummy/storage/*.sqlite3 +/test/dummy/storage/*.sqlite3-* /test/dummy/db/*.sqlite3 /test/dummy/db/*.sqlite3-* /test/dummy/log/*.log diff --git a/actionmailbox/CHANGELOG.md b/actionmailbox/CHANGELOG.md index bb9960aec9c65..33398e8780b7f 100644 --- a/actionmailbox/CHANGELOG.md +++ b/actionmailbox/CHANGELOG.md @@ -1,30 +1,5 @@ -* Update Mandrill inbound email route to respond appropriately to HEAD requests for URL health checks from Mandrill. +* Add `reply_to_address` extension method on `Mail::Message`. - *Bill Cromie* + *Mr0grog* -* Add way to deliver emails via source instead of filling out a form through the conductor interface. - - *DHH* - -* Mailgun ingress now passes through the envelope recipient as `X-Original-To`. - - *Rikki Pitt* - -* Deprecate `Rails.application.credentials.action_mailbox.api_key` and `MAILGUN_INGRESS_API_KEY` in favor of `Rails.application.credentials.action_mailbox.signing_key` and `MAILGUN_INGRESS_SIGNING_KEY`. - - *Matthijs Vos* - -* Allow easier creation of multi-part emails from the `create_inbound_email_from_mail` and `receive_inbound_email_from_mail` test helpers. - - *Michael Herold* - -* Fix Bcc header not being included with emails from `create_inbound_email_from` test helpers. - - *jduff* - -* Add `ApplicationMailbox.mailbox_for` to expose mailbox routing. - - *James Dabbs* - - -Please check [6-0-stable](https://github.com/rails/rails/blob/6-0-stable/actionmailbox/CHANGELOG.md) for previous changes. +Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/actionmailbox/CHANGELOG.md) for previous changes. diff --git a/actionmailbox/MIT-LICENSE b/actionmailbox/MIT-LICENSE index 8199339349a23..18b341857b9f9 100644 --- a/actionmailbox/MIT-LICENSE +++ b/actionmailbox/MIT-LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2020 Basecamp, LLC +Copyright (c) 37signals LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/actionmailbox/README.md b/actionmailbox/README.md index 593bd429ae27d..47fcbc0113fe1 100644 --- a/actionmailbox/README.md +++ b/actionmailbox/README.md @@ -1,12 +1,12 @@ # Action Mailbox -Action Mailbox routes incoming emails to controller-like mailboxes for processing in Rails. It ships with ingresses for Mailgun, Mandrill, Postmark, and SendGrid. You can also handle inbound mails directly via the built-in Exim, Postfix, and Qmail ingresses. +Action Mailbox routes incoming emails to controller-like mailboxes for processing in \Rails. It ships with ingresses for Mailgun, Mandrill, Postmark, and SendGrid. You can also handle inbound mails directly via the built-in Exim, Postfix, and Qmail ingresses. The inbound emails are turned into `InboundEmail` records using Active Record and feature lifecycle tracking, storage of the original email on cloud storage via Active Storage, and responsible data handling with on-by-default incineration. These inbound emails are routed asynchronously using Active Job to one or several dedicated mailboxes, which are capable of interacting directly with the rest of your domain model. -You can read more about Action Mailbox in the [Action Mailbox Basics](https://edgeguides.rubyonrails.org/action_mailbox_basics.html) guide. +You can read more about Action Mailbox in the [Action Mailbox Basics](https://guides.rubyonrails.org/action_mailbox_basics.html) guide. ## License diff --git a/actionmailbox/Rakefile b/actionmailbox/Rakefile index 36aed17282614..45e88e82e16d2 100644 --- a/actionmailbox/Rakefile +++ b/actionmailbox/Rakefile @@ -4,12 +4,21 @@ require "bundler/setup" require "bundler/gem_tasks" require "rake/testtask" -task :package +ENV["RAILS_MINITEST_PLUGIN"] = "true" Rake::TestTask.new do |t| t.libs << "test" t.pattern = "test/**/*_test.rb" t.verbose = true + t.options = "--profile" if ENV["CI"] +end + +namespace :test do + task :isolated do + FileList["test/**/*_test.rb"].exclude("test/dummy/**/*").all? do |file| + sh(Gem.ruby, "-w", "-Ilib", "-Itest", file) + end || raise("Failures") + end end task default: :test diff --git a/actionmailbox/actionmailbox.gemspec b/actionmailbox/actionmailbox.gemspec index 47501b1cd19e9..7496467f95f73 100644 --- a/actionmailbox/actionmailbox.gemspec +++ b/actionmailbox/actionmailbox.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.summary = "Inbound email handling framework." s.description = "Receive and process incoming emails in Rails applications." - s.required_ruby_version = ">= 2.5.0" + s.required_ruby_version = ">= 3.2.0" s.license = "MIT" @@ -26,6 +26,7 @@ Gem::Specification.new do |s| "documentation_uri" => "https://api.rubyonrails.org/v#{version}/", "mailing_list_uri" => "https://discuss.rubyonrails.org/c/rubyonrails-talk", "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actionmailbox", + "rubygems_mfa_required" => "true", } # NOTE: Please read our dependency guidelines before updating versions: @@ -37,5 +38,5 @@ Gem::Specification.new do |s| s.add_dependency "activejob", version s.add_dependency "actionpack", version - s.add_dependency "mail", ">= 2.7.1" + s.add_dependency "mail", ">= 2.8.0" end diff --git a/actionmailbox/app/controllers/action_mailbox/base_controller.rb b/actionmailbox/app/controllers/action_mailbox/base_controller.rb index 80a14355b776a..fdd3b5e735aa8 100644 --- a/actionmailbox/app/controllers/action_mailbox/base_controller.rb +++ b/actionmailbox/app/controllers/action_mailbox/base_controller.rb @@ -3,7 +3,7 @@ module ActionMailbox # The base class for all Action Mailbox ingress controllers. class BaseController < ActionController::Base - skip_forgery_protection if default_protect_from_forgery + skip_forgery_protection before_action :ensure_configured diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb index 42f4fee1fa4c7..c14e5334755e8 100644 --- a/actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +++ b/actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb @@ -44,6 +44,7 @@ module ActionMailbox # https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime. class Ingresses::Mailgun::InboundEmailsController < ActionMailbox::BaseController before_action :authenticate + param_encoding :create, "body-mime", Encoding::ASCII_8BIT def create ActionMailbox::InboundEmail.create_and_extract_message_id! mail @@ -77,21 +78,7 @@ def authenticated? end def key - if Rails.application.credentials.dig(:action_mailbox, :mailgun_api_key) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Rails.application.credentials.action_mailbox.api_key is deprecated and will be ignored in Rails 6.2. - Use Rails.application.credentials.action_mailbox.signing_key instead. - MSG - Rails.application.credentials.dig(:action_mailbox, :mailgun_api_key) - elsif ENV["MAILGUN_INGRESS_API_KEY"] - ActiveSupport::Deprecation.warn(<<-MSG.squish) - The MAILGUN_INGRESS_API_KEY environment variable is deprecated and will be ignored in Rails 6.2. - Use MAILGUN_INGRESS_SIGNING_KEY instead. - MSG - ENV["MAILGUN_INGRESS_API_KEY"] - else - Rails.application.credentials.dig(:action_mailbox, :mailgun_signing_key) || ENV["MAILGUN_INGRESS_SIGNING_KEY"] - end + Rails.application.credentials.dig(:action_mailbox, :mailgun_signing_key) || ENV["MAILGUN_INGRESS_SIGNING_KEY"] end class Authenticator diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb index 4f7db02faebe7..db4e141627cea 100644 --- a/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb +++ b/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb @@ -46,9 +46,10 @@ module ActionMailbox # content in JSON payload"*. Action Mailbox needs the raw email content to work. class Ingresses::Postmark::InboundEmailsController < ActionMailbox::BaseController before_action :authenticate_by_password + param_encoding :create, "RawEmail", Encoding::ASCII_8BIT def create - ActionMailbox::InboundEmail.create_and_extract_message_id! params.require("RawEmail") + ActionMailbox::InboundEmail.create_and_extract_message_id! mail rescue ActionController::ParameterMissing => error logger.error <<~MESSAGE #{error.message} @@ -58,5 +59,12 @@ def create MESSAGE head :unprocessable_entity end + + private + def mail + params.require("RawEmail").tap do |raw_email| + raw_email.prepend("X-Original-To: ", params.require("OriginalRecipient"), "\n") if params.key?("OriginalRecipient") + end + end end end diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb index ba0afb737e183..d569e04334bd1 100644 --- a/actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb +++ b/actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb @@ -41,23 +41,27 @@ module ActionMailbox # If your application lives at https://example.com, you would configure the Postfix SMTP server to pipe # inbound emails to the following command: # - # bin/rails action_mailbox:ingress:postfix URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... + # $ bin/rails action_mailbox:ingress:postfix URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... # # Built-in ingress commands are available for these popular SMTP servers: # - # - Exim (bin/rails action_mailbox:ingress:exim) - # - Postfix (bin/rails action_mailbox:ingress:postfix) - # - Qmail (bin/rails action_mailbox:ingress:qmail) + # - Exim (bin/rails action_mailbox:ingress:exim) + # - Postfix (bin/rails action_mailbox:ingress:postfix) + # - Qmail (bin/rails action_mailbox:ingress:qmail) class Ingresses::Relay::InboundEmailsController < ActionMailbox::BaseController before_action :authenticate_by_password, :require_valid_rfc822_message def create - ActionMailbox::InboundEmail.create_and_extract_message_id! request.body.read + if request.body + ActionMailbox::InboundEmail.create_and_extract_message_id! request.body.read + else + head :unprocessable_entity + end end private def require_valid_rfc822_message - unless request.content_type == "message/rfc822" + unless request.media_type == "message/rfc822" head :unsupported_media_type end end diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb index a58beea530a6f..fc2d7aba926fa 100644 --- a/actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +++ b/actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb @@ -46,9 +46,24 @@ module ActionMailbox # full MIME message."* Action Mailbox needs the raw MIME message to work. class Ingresses::Sendgrid::InboundEmailsController < ActionMailbox::BaseController before_action :authenticate_by_password + param_encoding :create, :email, Encoding::ASCII_8BIT def create - ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:email) + ActionMailbox::InboundEmail.create_and_extract_message_id! mail + rescue JSON::ParserError => error + logger.error error.message + head :unprocessable_entity end + + private + def mail + params.require(:email).tap do |raw_email| + envelope["to"].each { |to| raw_email.prepend("X-Original-To: ", to, "\n") } if params.key?(:envelope) + end + end + + def envelope + JSON.parse(params.require(:envelope)) + end end end diff --git a/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails/sources_controller.rb b/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails/sources_controller.rb index bfc013e9bcf10..dfc382c7dcdeb 100644 --- a/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails/sources_controller.rb +++ b/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails/sources_controller.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true +# :enddoc: + module Rails - class Conductor::ActionMailbox::InboundEmails::SourcesController < Rails::Conductor::BaseController + class Conductor::ActionMailbox::InboundEmails::SourcesController < Rails::Conductor::BaseController # :nodoc: def new end diff --git a/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb b/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb index 4846739c456dd..1146082b4b2b7 100644 --- a/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb +++ b/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :enddoc: + module Rails class Conductor::ActionMailbox::InboundEmailsController < Rails::Conductor::BaseController def index @@ -20,14 +22,18 @@ def create private def new_mail - Mail.new(params.require(:mail).permit(:from, :to, :cc, :bcc, :x_original_to, :in_reply_to, :subject, :body).to_h).tap do |mail| + Mail.new(mail_params.except(:attachments).to_h).tap do |mail| mail[:bcc]&.include_in_headers = true - params[:mail][:attachments].to_a.each do |attachment| + mail_params[:attachments]&.select(&:present?)&.each do |attachment| mail.add_file(filename: attachment.original_filename, content: attachment.read) end end end + def mail_params + params.expect(mail: [:from, :to, :cc, :bcc, :x_original_to, :in_reply_to, :subject, :body, attachments: []]) + end + def create_inbound_email(mail) ActionMailbox::InboundEmail.create_and_extract_message_id!(mail.to_s) end diff --git a/actionmailbox/app/controllers/rails/conductor/action_mailbox/incinerates_controller.rb b/actionmailbox/app/controllers/rails/conductor/action_mailbox/incinerates_controller.rb new file mode 100644 index 0000000000000..2466eda8eac63 --- /dev/null +++ b/actionmailbox/app/controllers/rails/conductor/action_mailbox/incinerates_controller.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# :enddoc: + +module Rails + # Incinerating will destroy an email that is due and has already been processed. + class Conductor::ActionMailbox::IncineratesController < Rails::Conductor::BaseController + def create + ActionMailbox::InboundEmail.find(params[:inbound_email_id]).incinerate + + redirect_to main_app.rails_conductor_inbound_emails_url + end + end +end diff --git a/actionmailbox/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb b/actionmailbox/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb index 05b1ca9a39d9c..a1f266d00b88d 100644 --- a/actionmailbox/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb +++ b/actionmailbox/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :enddoc: + module Rails # Rerouting will run routing and processing on an email that has already been, or attempted to be, processed. class Conductor::ActionMailbox::ReroutesController < Rails::Conductor::BaseController diff --git a/actionmailbox/app/models/action_mailbox/inbound_email.rb b/actionmailbox/app/models/action_mailbox/inbound_email.rb index 023de19ccc6cb..76af7caf922f1 100644 --- a/actionmailbox/app/models/action_mailbox/inbound_email.rb +++ b/actionmailbox/app/models/action_mailbox/inbound_email.rb @@ -24,13 +24,11 @@ module ActionMailbox # # inbound_email.mail.from # => 'david@loudthinking.com' # inbound_email.source # Returns the full rfc822 source of the email as text - class InboundEmail < ActiveRecord::Base - self.table_name = "action_mailbox_inbound_emails" - + class InboundEmail < Record include Incineratable, MessageId, Routable - has_one_attached :raw_email - enum status: %i[ pending processing delivered failed bounced ] + has_one_attached :raw_email, service: ActionMailbox.storage_service + enum :status, %i[ pending processing delivered failed bounced ] def mail @mail ||= Mail.from_source(source) @@ -43,6 +41,14 @@ def source def processed? delivered? || failed? || bounced? end + + def instrumentation_payload # :nodoc: + { + id: id, + message_id: message_id, + status: status + } + end end end diff --git a/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb b/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb index 381061c90f92e..78392f02ee772 100644 --- a/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb +++ b/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb @@ -5,7 +5,7 @@ # web request. # # If an inbound email does not, against the rfc822 mandate, specify a Message-ID, one will be generated -# using the approach from Mail::MessageIdField. +# using the approach from +Mail::MessageIdField+. module ActionMailbox::InboundEmail::MessageId extend ActiveSupport::Concern @@ -14,12 +14,11 @@ module ActionMailbox::InboundEmail::MessageId # attachment called +raw_email+. Before the upload, extract the Message-ID from the +source+ and set # it as an attribute on the new +InboundEmail+. def create_and_extract_message_id!(source, **options) - message_checksum = Digest::SHA1.hexdigest(source) + message_checksum = OpenSSL::Digest::SHA1.hexdigest(source) message_id = extract_message_id(source) || generate_missing_message_id(message_checksum) - create! options.merge(message_id: message_id, message_checksum: message_checksum) do |inbound_email| - inbound_email.raw_email.attach io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822" - end + create! raw_email: create_and_upload_raw_email!(source), + message_id: message_id, message_checksum: message_checksum, **options rescue ActiveRecord::RecordNotUnique nil end @@ -34,5 +33,10 @@ def generate_missing_message_id(message_checksum) logger.warn "Message-ID couldn't be parsed or is missing. Generated a new Message-ID: #{message_id}" end end + + def create_and_upload_raw_email!(source) + ActiveStorage::Blob.create_and_upload! io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822", + service_name: ActionMailbox.storage_service + end end end diff --git a/actionmailbox/app/models/action_mailbox/record.rb b/actionmailbox/app/models/action_mailbox/record.rb new file mode 100644 index 0000000000000..77b3a99e8978f --- /dev/null +++ b/actionmailbox/app/models/action_mailbox/record.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module ActionMailbox + class Record < ActiveRecord::Base # :nodoc: + self.abstract_class = true + end +end + +ActiveSupport.run_load_hooks :action_mailbox_record, ActionMailbox::Record diff --git a/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb index dd501b398c527..65f64bf56d7a9 100644 --- a/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb +++ b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb @@ -2,6 +2,9 @@

All inbound emails

+<%= link_to "New inbound email by form", main_app.new_rails_conductor_inbound_email_path %> | +<%= link_to "New inbound email by source", main_app.new_rails_conductor_inbound_email_source_path %> + <% @inbound_emails.each do |inbound_email| %> @@ -11,6 +14,3 @@ <% end %>
Message IDStatus
- -<%= link_to "New inbound email by form", main_app.new_rails_conductor_inbound_email_path %> | -<%= link_to "New inbound email by source", main_app.new_rails_conductor_inbound_email_source_path %> diff --git a/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb index 752e0c31872dc..5b0e639509a6d 100644 --- a/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb +++ b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb @@ -5,42 +5,42 @@ <%= form_with(url: main_app.rails_conductor_inbound_emails_path, scope: :mail, local: true) do |form| %>
<%= form.label :from, "From" %>
- <%= form.text_field :from %> + <%= form.text_field :from, value: params[:from] %>
<%= form.label :to, "To" %>
- <%= form.text_field :to %> + <%= form.text_field :to, value: params[:to] %>
<%= form.label :cc, "CC" %>
- <%= form.text_field :cc %> + <%= form.text_field :cc, value: params[:cc] %>
<%= form.label :bcc, "BCC" %>
- <%= form.text_field :bcc %> + <%= form.text_field :bcc, value: params[:bcc] %>
<%= form.label :x_original_to, "X-Original-To" %>
- <%= form.text_field :x_original_to %> + <%= form.text_field :x_original_to, value: params[:x_original_to] %>
<%= form.label :in_reply_to, "In-Reply-To" %>
- <%= form.text_field :in_reply_to %> + <%= form.text_field :in_reply_to, value: params[:in_reply_to] %>
<%= form.label :subject, "Subject" %>
- <%= form.text_field :subject %> + <%= form.text_field :subject, value: params[:subject] %>
<%= form.label :body, "Body" %>
- <%= form.text_area :body, size: "40x20" %> + <%= form.textarea :body, size: "40x20", value: params[:body] %>
diff --git a/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb index e7619041960c3..48c08cd040ad3 100644 --- a/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb +++ b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb @@ -4,7 +4,7 @@
  • <%= button_to "Route again", main_app.rails_conductor_inbound_email_reroute_path(@inbound_email), method: :post %>
  • -
  • Incinerate
  • +
  • <%= button_to "Incinerate", main_app.rails_conductor_inbound_email_incinerate_path(@inbound_email), method: :post %>
diff --git a/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/sources/new.html.erb b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/sources/new.html.erb index 637efeda03155..82b6ea8dd1bb9 100644 --- a/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/sources/new.html.erb +++ b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/sources/new.html.erb @@ -5,7 +5,7 @@ <%= form_with(url: main_app.rails_conductor_inbound_email_sources_path, local: true) do |form| %>
<%= form.label :source, "Source" %>
- <%= form.text_area :source, size: "80x60" %> + <%= form.textarea :source, size: "80x60" %>
<%= form.submit "Deliver inbound email" %> diff --git a/actionmailbox/config/routes.rb b/actionmailbox/config/routes.rb index 8f48fc817864f..e3d5737a631f3 100644 --- a/actionmailbox/config/routes.rb +++ b/actionmailbox/config/routes.rb @@ -16,10 +16,11 @@ # TODO: Should these be mounted within the engine only? scope "rails/conductor/action_mailbox/", module: "rails/conductor/action_mailbox" do - resources :inbound_emails, as: :rails_conductor_inbound_emails + resources :inbound_emails, as: :rails_conductor_inbound_emails, only: %i[index new show create] get "inbound_emails/sources/new", to: "inbound_emails/sources#new", as: :new_rails_conductor_inbound_email_source post "inbound_emails/sources", to: "inbound_emails/sources#create", as: :rails_conductor_inbound_email_sources post ":inbound_email_id/reroute" => "reroutes#create", as: :rails_conductor_inbound_email_reroute + post ":inbound_email_id/incinerate" => "incinerates#create", as: :rails_conductor_inbound_email_incinerate end end diff --git a/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb b/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb index 2bf4335808976..2a1e2f7a8e2e1 100644 --- a/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb +++ b/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb @@ -1,6 +1,6 @@ class CreateActionMailboxTables < ActiveRecord::Migration[6.0] def change - create_table :action_mailbox_inbound_emails do |t| + create_table :action_mailbox_inbound_emails, id: primary_key_type do |t| t.integer :status, default: 0, null: false t.string :message_id, null: false t.string :message_checksum, null: false @@ -10,4 +10,10 @@ def change t.index [ :message_id, :message_checksum ], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true end end + + private + def primary_key_type + config = Rails.configuration.generators + config.options[config.orm][:primary_key_type] || :primary_key + end end diff --git a/actionmailbox/lib/action_mailbox.rb b/actionmailbox/lib/action_mailbox.rb index 772dbd6529246..d0e6b1600d840 100644 --- a/actionmailbox/lib/action_mailbox.rb +++ b/actionmailbox/lib/action_mailbox.rb @@ -1,7 +1,15 @@ # frozen_string_literal: true +require "active_support" +require "active_support/rails" +require "active_support/core_ext/numeric/time" + +require "action_mailbox/version" +require "action_mailbox/deprecator" require "action_mailbox/mail_ext" +# :markup: markdown +# :include: ../README.md module ActionMailbox extend ActiveSupport::Autoload @@ -14,4 +22,5 @@ module ActionMailbox mattr_accessor :incinerate, default: true mattr_accessor :incinerate_after, default: 30.days mattr_accessor :queues, default: {} + mattr_accessor :storage_service end diff --git a/actionmailbox/lib/action_mailbox/base.rb b/actionmailbox/lib/action_mailbox/base.rb index ff8587acd1c6d..d6ff9caf404bb 100644 --- a/actionmailbox/lib/action_mailbox/base.rb +++ b/actionmailbox/lib/action_mailbox/base.rb @@ -6,6 +6,8 @@ require "action_mailbox/routing" module ActionMailbox + # = Action Mailbox \Base + # # The base class for all application mailboxes. Not intended to be inherited from directly. Inherit from # +ApplicationMailbox+ instead, as that's where the app-specific routing is configured. This routing # is specified in the following ways: @@ -27,15 +29,15 @@ module ActionMailbox # routing :all => :backstop # end # - # Application mailboxes need to overwrite the +#process+ method, which is invoked by the framework after + # Application mailboxes need to override the #process method, which is invoked by the framework after # callbacks have been run. The callbacks available are: +before_processing+, +after_processing+, and - # +around_processing+. The primary use case is ensure certain preconditions to processing are fulfilled + # +around_processing+. The primary use case is to ensure that certain preconditions to processing are fulfilled # using +before_processing+ callbacks. # # If a precondition fails to be met, you can halt the processing using the +#bounced!+ method, # which will silently prevent any further processing, but not actually send out any bounce notice. You # can also pair this behavior with the invocation of an Action Mailer class responsible for sending out - # an actual bounce email. This is done using the +#bounce_with+ method, which takes the mail object returned + # an actual bounce email. This is done using the #bounce_with method, which takes the mail object returned # by an Action Mailer method, like so: # # class ForwardsMailbox < ApplicationMailbox @@ -51,11 +53,12 @@ module ActionMailbox # # During the processing of the inbound email, the status will be tracked. Before processing begins, # the email will normally have the +pending+ status. Once processing begins, just before callbacks - # and the +#process+ method is called, the status is changed to +processing+. If processing is allowed to + # and the #process method is called, the status is changed to +processing+. If processing is allowed to # complete, the status is changed to +delivered+. If a bounce is triggered, then +bounced+. If an unhandled # exception is bubbled up, then +failed+. # - # Exceptions can be handled at the class level using the familiar +Rescuable+ approach: + # Exceptions can be handled at the class level using the familiar + # ActiveSupport::Rescuable approach: # # class ForwardsMailbox < ApplicationMailbox # rescue_from(ApplicationSpecificVerificationError) { bounced! } @@ -77,33 +80,47 @@ def initialize(inbound_email) @inbound_email = inbound_email end - def perform_processing #:nodoc: - track_status_of_inbound_email do - run_callbacks :process do - process + def perform_processing # :nodoc: + ActiveSupport::Notifications.instrument "process.action_mailbox", instrumentation_payload do + track_status_of_inbound_email do + run_callbacks :process do + process + end end + rescue => exception + # TODO: Include a reference to the inbound_email in the exception raised so error handling becomes easier + rescue_with_handler(exception) || raise end - rescue => exception - # TODO: Include a reference to the inbound_email in the exception raised so error handling becomes easier - rescue_with_handler(exception) || raise end def process - # Overwrite in subclasses + # Override in subclasses end - def finished_processing? #:nodoc: + def finished_processing? # :nodoc: inbound_email.delivered? || inbound_email.bounced? end - # Enqueues the given +message+ for delivery and changes the inbound email's status to +:bounced+. def bounce_with(message) inbound_email.bounced! message.deliver_later end + # Immediately sends the given +message+ and changes the inbound email's status to +:bounced+. + def bounce_now_with(message) + inbound_email.bounced! + message.deliver_now + end + private + def instrumentation_payload + { + mailbox: self, + inbound_email: inbound_email.instrumentation_payload + } + end + def track_status_of_inbound_email inbound_email.processing! yield diff --git a/actionmailbox/lib/action_mailbox/callbacks.rb b/actionmailbox/lib/action_mailbox/callbacks.rb index 2b7212284bd0e..cc801083dd091 100644 --- a/actionmailbox/lib/action_mailbox/callbacks.rb +++ b/actionmailbox/lib/action_mailbox/callbacks.rb @@ -3,6 +3,8 @@ require "active_support/callbacks" module ActionMailbox + # = Action Mailbox \Callbacks + # # Defines the callbacks related to processing. module Callbacks extend ActiveSupport::Concern diff --git a/actionmailbox/lib/action_mailbox/deprecator.rb b/actionmailbox/lib/action_mailbox/deprecator.rb new file mode 100644 index 0000000000000..675becfc42921 --- /dev/null +++ b/actionmailbox/lib/action_mailbox/deprecator.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ActionMailbox + def self.deprecator # :nodoc: + @deprecator ||= ActiveSupport::Deprecation.new + end +end diff --git a/actionmailbox/lib/action_mailbox/engine.rb b/actionmailbox/lib/action_mailbox/engine.rb index bab3964d9367f..d0dc5c8ce29a7 100644 --- a/actionmailbox/lib/action_mailbox/engine.rb +++ b/actionmailbox/lib/action_mailbox/engine.rb @@ -20,6 +20,12 @@ class Engine < Rails::Engine config.action_mailbox.queues = ActiveSupport::InheritableOptions.new \ incineration: :action_mailbox_incineration, routing: :action_mailbox_routing + config.action_mailbox.storage_service = nil + + initializer "action_mailbox.deprecator", before: :load_environment_config do |app| + app.deprecators[:action_mailbox] = ActionMailbox.deprecator + end + initializer "action_mailbox.config" do config.after_initialize do |app| ActionMailbox.logger = app.config.action_mailbox.logger || Rails.logger @@ -27,6 +33,7 @@ class Engine < Rails::Engine ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days ActionMailbox.queues = app.config.action_mailbox.queues || {} ActionMailbox.ingress = app.config.action_mailbox.ingress + ActionMailbox.storage_service = app.config.action_mailbox.storage_service end end end diff --git a/actionmailbox/lib/action_mailbox/gem_version.rb b/actionmailbox/lib/action_mailbox/gem_version.rb index 5ad2400451ab8..d351800d34bbb 100644 --- a/actionmailbox/lib/action_mailbox/gem_version.rb +++ b/actionmailbox/lib/action_mailbox/gem_version.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true module ActionMailbox - # Returns the currently-loaded version of Action Mailbox as a Gem::Version. + # Returns the currently loaded version of Action Mailbox as a +Gem::Version+. def self.gem_version Gem::Version.new VERSION::STRING end module VERSION - MAJOR = 6 + MAJOR = 8 MINOR = 1 TINY = 0 PRE = "alpha" diff --git a/actionmailbox/lib/action_mailbox/mail_ext/addresses.rb b/actionmailbox/lib/action_mailbox/mail_ext/addresses.rb index 5eab1feb3d563..5961088c781c0 100644 --- a/actionmailbox/lib/action_mailbox/mail_ext/addresses.rb +++ b/actionmailbox/lib/action_mailbox/mail_ext/addresses.rb @@ -3,27 +3,46 @@ module Mail class Message def from_address - header[:from]&.address_list&.addresses&.first + address_list(header[:from])&.addresses&.first + end + + def reply_to_address + address_list(header[:reply_to])&.addresses&.first end def recipients_addresses - to_addresses + cc_addresses + bcc_addresses + x_original_to_addresses + to_addresses + cc_addresses + bcc_addresses + x_original_to_addresses + x_forwarded_to_addresses end def to_addresses - Array(header[:to]&.address_list&.addresses) + Array(address_list(header[:to])&.addresses) end def cc_addresses - Array(header[:cc]&.address_list&.addresses) + Array(address_list(header[:cc])&.addresses) end def bcc_addresses - Array(header[:bcc]&.address_list&.addresses) + Array(address_list(header[:bcc])&.addresses) end def x_original_to_addresses Array(header[:x_original_to]).collect { |header| Mail::Address.new header.to_s } end + + def x_forwarded_to_addresses + Array(header[:x_forwarded_to]).collect { |header| Mail::Address.new header.to_s } + end + + private + def address_list(obj) + if obj.respond_to?(:element) + # Mail 2.8+ + obj.element + else + # Mail <= 2.7.x + obj&.address_list + end + end end end diff --git a/actionmailbox/lib/action_mailbox/mail_ext/recipients.rb b/actionmailbox/lib/action_mailbox/mail_ext/recipients.rb index 1f8a713218830..102ec83debe4b 100644 --- a/actionmailbox/lib/action_mailbox/mail_ext/recipients.rb +++ b/actionmailbox/lib/action_mailbox/mail_ext/recipients.rb @@ -3,7 +3,8 @@ module Mail class Message def recipients - Array(to) + Array(cc) + Array(bcc) + Array(header[:x_original_to]).map(&:to_s) + Array(to) + Array(cc) + Array(bcc) + Array(header[:x_original_to]).map(&:to_s) + + Array(header[:x_forwarded_to]).map(&:to_s) end end end diff --git a/actionmailbox/lib/action_mailbox/router.rb b/actionmailbox/lib/action_mailbox/router.rb index 9ac58e5b3fb45..0b67266af5c83 100644 --- a/actionmailbox/lib/action_mailbox/router.rb +++ b/actionmailbox/lib/action_mailbox/router.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module ActionMailbox + # = Action Mailbox \Router + # # Encapsulates the routes that live on the ApplicationMailbox and performs the actual routing when # an inbound_email is received. class Router diff --git a/actionmailbox/lib/action_mailbox/router/route.rb b/actionmailbox/lib/action_mailbox/router/route.rb index 7e98e8338296d..7c67c775900f7 100644 --- a/actionmailbox/lib/action_mailbox/router/route.rb +++ b/actionmailbox/lib/action_mailbox/router/route.rb @@ -2,7 +2,7 @@ module ActionMailbox # Encapsulates a route, which can then be matched against an inbound_email and provide a lookup of the matching - # mailbox class. See examples for the different route addresses and how to use them in the +ActionMailbox::Base+ + # mailbox class. See examples for the different route addresses and how to use them in the ActionMailbox::Base # documentation. class Router::Route attr_reader :address, :mailbox_name diff --git a/actionmailbox/lib/action_mailbox/routing.rb b/actionmailbox/lib/action_mailbox/routing.rb index 8391bf9db0b13..4e98d4ee0b29f 100644 --- a/actionmailbox/lib/action_mailbox/routing.rb +++ b/actionmailbox/lib/action_mailbox/routing.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module ActionMailbox - # See +ActionMailbox::Base+ for how to specify routing. + # See ActionMailbox::Base for how to specify routing. module Routing extend ActiveSupport::Concern diff --git a/actionmailbox/lib/action_mailbox/test_helper.rb b/actionmailbox/lib/action_mailbox/test_helper.rb index 1be23ea8dbfd7..ea50afd7f0a61 100644 --- a/actionmailbox/lib/action_mailbox/test_helper.rb +++ b/actionmailbox/lib/action_mailbox/test_helper.rb @@ -4,18 +4,18 @@ module ActionMailbox module TestHelper - # Create an +InboundEmail+ record using an eml fixture in the format of message/rfc822 + # Create an InboundEmail record using an eml fixture in the format of message/rfc822 # referenced with +fixture_name+ located in +test/fixtures/files/fixture_name+. def create_inbound_email_from_fixture(fixture_name, status: :processing) create_inbound_email_from_source file_fixture(fixture_name).read, status: status end - # Creates an +InboundEmail+ by specifying through options or a block. + # Creates an InboundEmail by specifying through options or a block. # # ==== Options # - # * :status - The +status+ to set for the created +InboundEmail+. - # For possible statuses, see {its documentation}[rdoc-ref:ActionMailbox::InboundEmail]. + # * :status - The +status+ to set for the created InboundEmail. + # For possible statuses, see its documentation. # # ==== Creating a simple email # @@ -68,26 +68,25 @@ def create_inbound_email_from_mail(status: :processing, **mail_options, &block) create_inbound_email_from_source mail.to_s, status: status end - # Create an +InboundEmail+ using the raw rfc822 +source+ as text. + # Create an InboundEmail using the raw rfc822 +source+ as text. def create_inbound_email_from_source(source, status: :processing) ActionMailbox::InboundEmail.create_and_extract_message_id! source, status: status end - # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_fixture+ + # Create an InboundEmail from fixture using the same arguments as create_inbound_email_from_fixture # and immediately route it to processing. def receive_inbound_email_from_fixture(*args) create_inbound_email_from_fixture(*args).tap(&:route) end - # Create an +InboundEmail+ using the same options or block as - # {create_inbound_email_from_mail}[rdoc-ref:#create_inbound_email_from_mail], - # then immediately route it for processing. + # Create an InboundEmail using the same options or block as + # create_inbound_email_from_mail, then immediately route it for processing. def receive_inbound_email_from_mail(**kwargs, &block) create_inbound_email_from_mail(**kwargs, &block).tap(&:route) end - # Create an +InboundEmail+ using the same arguments as +create_inbound_email_from_source+ and immediately route it + # Create an InboundEmail using the same arguments as create_inbound_email_from_source and immediately route it # to processing. def receive_inbound_email_from_source(*args) create_inbound_email_from_source(*args).tap(&:route) diff --git a/actionmailbox/lib/action_mailbox/version.rb b/actionmailbox/lib/action_mailbox/version.rb index e65d27f5ddf84..10c4cee6adcf7 100644 --- a/actionmailbox/lib/action_mailbox/version.rb +++ b/actionmailbox/lib/action_mailbox/version.rb @@ -3,7 +3,7 @@ require_relative "gem_version" module ActionMailbox - # Returns the currently-loaded version of Action Mailbox as a Gem::Version. + # Returns the currently loaded version of Action Mailbox as a +Gem::Version+. def self.version gem_version end diff --git a/actionmailbox/lib/generators/action_mailbox/install/install_generator.rb b/actionmailbox/lib/generators/action_mailbox/install/install_generator.rb index e919b3355ac13..05066d6071d2c 100644 --- a/actionmailbox/lib/generators/action_mailbox/install/install_generator.rb +++ b/actionmailbox/lib/generators/action_mailbox/install/install_generator.rb @@ -21,7 +21,7 @@ def add_action_mailbox_production_environment_config end def create_migrations - rails_command "railties:install:migrations FROM=active_storage,action_mailbox" + rails_command "railties:install:migrations FROM=active_storage,action_mailbox", inline: true end end end diff --git a/actionmailbox/lib/rails/generators/mailbox/USAGE b/actionmailbox/lib/rails/generators/mailbox/USAGE index ef3510594849f..371fc2af69119 100644 --- a/actionmailbox/lib/rails/generators/mailbox/USAGE +++ b/actionmailbox/lib/rails/generators/mailbox/USAGE @@ -1,11 +1,9 @@ Description: -============ - Stubs out a new mailbox class in app/mailboxes and invokes your template + Generates a new mailbox class in app/mailboxes and invokes your template engine and test framework generators. Example: -======== - bin/rails generate mailbox inbox + `bin/rails generate mailbox inbox` creates an InboxMailbox class and test: Mailbox: app/mailboxes/inbox_mailbox.rb diff --git a/actionmailbox/lib/tasks/install.rake b/actionmailbox/lib/tasks/install.rake index fe6ceb6a19187..cffa6c9c0f431 100644 --- a/actionmailbox/lib/tasks/install.rake +++ b/actionmailbox/lib/tasks/install.rake @@ -1,6 +1,6 @@ # frozen_string_literal: true -desc "Installs Action Mailbox and its dependencies" +desc "Install Action Mailbox and its dependencies" task "action_mailbox:install" do Rails::Command.invoke :generate, ["action_mailbox:install"] end diff --git a/actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb index bacf57330814c..dfed9fa63c465 100644 --- a/actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb +++ b/actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb @@ -25,6 +25,24 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsControllerTest < ActionDis assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id end + test "receiving an inbound email from Mailgun with non UTF-8 characters" do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + travel_to "2018-10-09 15:15:00 EDT" + post rails_mailgun_inbound_emails_url, params: { + timestamp: 1539112500, + token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi", + signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc", + "body-mime" => file_fixture("../files/invalid_utf.eml").read + } + end + + assert_response :no_content + + inbound_email = ActionMailbox::InboundEmail.last + assert_equal file_fixture("../files/invalid_utf.eml").binread, inbound_email.raw_email.download + assert_equal "05988AA6EC0D44318855A5E39E3B6F9E@jansterba.com", inbound_email.message_id + end + test "add X-Original-To to email from Mailgun" do assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do travel_to "2018-10-09 15:15:00 EDT" diff --git a/actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb index 7e73c397c4c6f..7dc900aa68c46 100644 --- a/actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb +++ b/actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb @@ -19,7 +19,7 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsControllerTest < ActionDi test "receiving an inbound email from Mandrill" do assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do post rails_mandrill_inbound_emails_url, - headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events } + headers: { "X-Mandrill-Signature" => "1bNbyqkMFL4VYIT5+RQCrPs/mRY=" }, params: { mandrill_events: @events } end assert_response :ok diff --git a/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb index 11b579b39cd0a..c371ad0cbae80 100644 --- a/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb +++ b/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb @@ -18,6 +18,35 @@ class ActionMailbox::Ingresses::Postmark::InboundEmailsControllerTest < ActionDi assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id end + test "receiving an inbound email from Postmark with non UTF-8 characters" do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_postmark_inbound_emails_url, + headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/invalid_utf.eml").read } + end + + assert_response :no_content + + inbound_email = ActionMailbox::InboundEmail.last + assert_equal file_fixture("../files/invalid_utf.eml").binread, inbound_email.raw_email.download + assert_equal "05988AA6EC0D44318855A5E39E3B6F9E@jansterba.com", inbound_email.message_id + end + + test "add X-Original-To to email from Postmark" do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_postmark_inbound_emails_url, + headers: { authorization: credentials }, params: { + RawEmail: file_fixture("../files/welcome.eml").read, + OriginalRecipient: "thisguy@domain.abcd", + } + end + + assert_response :no_content + + inbound_email = ActionMailbox::InboundEmail.last + mail = Mail.from_source(inbound_email.raw_email.download) + assert_equal "thisguy@domain.abcd", mail.header["X-Original-To"].decoded + end + test "rejecting when RawEmail param is missing" do assert_no_difference -> { ActionMailbox::InboundEmail.count } do post rails_postmark_inbound_emails_url, diff --git a/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb index 67c5993f7f35c..ac0a6fab75093 100644 --- a/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb +++ b/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb @@ -18,6 +18,28 @@ class ActionMailbox::Ingresses::Relay::InboundEmailsControllerTest < ActionDispa assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id end + test "receiving an inbound email relayed from an SMTP server with non UTF-8 characters" do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, + params: file_fixture("../files/invalid_utf.eml").read + end + + assert_response :no_content + + inbound_email = ActionMailbox::InboundEmail.last + assert_equal file_fixture("../files/invalid_utf.eml").binread, inbound_email.raw_email.download + assert_equal "05988AA6EC0D44318855A5E39E3B6F9E@jansterba.com", inbound_email.message_id + end + + test "rejecting a request with no body" do + assert_no_difference -> { ActionMailbox::InboundEmail.count } do + post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, + env: { "rack.input" => nil } + end + + assert_response :unprocessable_entity + end + test "rejecting an unauthorized inbound email" do assert_no_difference -> { ActionMailbox::InboundEmail.count } do post rails_relay_inbound_emails_url, headers: { "Content-Type" => "message/rfc822" }, diff --git a/actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb index 59a87e92485c2..3454585088e91 100644 --- a/actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb +++ b/actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb @@ -18,6 +18,35 @@ class ActionMailbox::Ingresses::Sendgrid::InboundEmailsControllerTest < ActionDi assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id end + test "receiving an inbound email from Sendgrid with non UTF-8 characters" do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_sendgrid_inbound_emails_url, + headers: { authorization: credentials }, params: { email: file_fixture("../files/invalid_utf.eml").read } + end + + assert_response :no_content + + inbound_email = ActionMailbox::InboundEmail.last + assert_equal file_fixture("../files/invalid_utf.eml").binread, inbound_email.raw_email.download + assert_equal "05988AA6EC0D44318855A5E39E3B6F9E@jansterba.com", inbound_email.message_id + end + + test "add X-Original-To to email from Sendgrid" do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_sendgrid_inbound_emails_url, + headers: { authorization: credentials }, params: { + email: file_fixture("../files/welcome.eml").read, + envelope: "{\"to\":[\"replies@example.com\"],\"from\":\"jason@37signals.com\"}", + } + end + + assert_response :no_content + + inbound_email = ActionMailbox::InboundEmail.last + mail = Mail.from_source(inbound_email.raw_email.download) + assert_equal "replies@example.com", mail.header["X-Original-To"].decoded + end + test "rejecting an unauthorized inbound email from Sendgrid" do assert_no_difference -> { ActionMailbox::InboundEmail.count } do post rails_sendgrid_inbound_emails_url, params: { email: file_fixture("../files/welcome.eml").read } diff --git a/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb index 33282d36f764d..83c9e90dd5671 100644 --- a/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb +++ b/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb @@ -60,7 +60,7 @@ class Rails::Conductor::ActionMailbox::InboundEmailsControllerTest < ActionDispa to: "Replies ", subject: "Let's debate some attachments", body: "Let's talk about these images:", - attachments: [ fixture_file_upload("files/avatar1.jpeg"), fixture_file_upload("files/avatar2.jpeg") ] + attachments: [ fixture_file_upload("avatar1.jpeg"), fixture_file_upload("avatar2.jpeg") ] } } end @@ -72,6 +72,29 @@ class Rails::Conductor::ActionMailbox::InboundEmailsControllerTest < ActionDispa end end + test "create inbound email with empty attachment" do + with_rails_env("development") do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_conductor_inbound_emails_path, params: { + mail: { + from: "", + to: "", + cc: "", + bcc: "", + x_original_to: "", + subject: "", + in_reply_to: "", + body: "", + attachments: [ "" ], + } + } + end + + mail = ActionMailbox::InboundEmail.last.mail + assert_equal 0, mail.attachments.count + end + end + private def with_rails_env(env) old_rails_env = Rails.env diff --git a/actionmailbox/test/dummy/.babelrc b/actionmailbox/test/dummy/.babelrc deleted file mode 100644 index ded31c0d80df4..0000000000000 --- a/actionmailbox/test/dummy/.babelrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "presets": [ - ["env", { - "modules": false, - "targets": { - "browsers": "> 1%", - "uglify": true - }, - "useBuiltIns": true - }] - ], - - "plugins": [ - "syntax-dynamic-import", - "transform-object-rest-spread", - ["transform-class-properties", { "spec": true }] - ] -} diff --git a/actionmailbox/test/dummy/.postcssrc.yml b/actionmailbox/test/dummy/.postcssrc.yml deleted file mode 100644 index 150dac3c6c6c3..0000000000000 --- a/actionmailbox/test/dummy/.postcssrc.yml +++ /dev/null @@ -1,3 +0,0 @@ -plugins: - postcss-import: {} - postcss-cssnext: {} diff --git a/actionmailbox/test/dummy/app/assets/config/manifest.js b/actionmailbox/test/dummy/app/assets/config/manifest.js index b16e53d6d56d2..591819335f0b2 100644 --- a/actionmailbox/test/dummy/app/assets/config/manifest.js +++ b/actionmailbox/test/dummy/app/assets/config/manifest.js @@ -1,3 +1,2 @@ //= link_tree ../images -//= link_directory ../javascripts .js //= link_directory ../stylesheets .css diff --git a/actionmailbox/test/dummy/app/jobs/application_job.rb b/actionmailbox/test/dummy/app/jobs/application_job.rb index a009ace51ccf4..d394c3d106230 100644 --- a/actionmailbox/test/dummy/app/jobs/application_job.rb +++ b/actionmailbox/test/dummy/app/jobs/application_job.rb @@ -1,2 +1,7 @@ class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError end diff --git a/actionmailbox/test/dummy/app/mailers/application_mailer.rb b/actionmailbox/test/dummy/app/mailers/application_mailer.rb index 286b2239d1399..3c34c8148f105 100644 --- a/actionmailbox/test/dummy/app/mailers/application_mailer.rb +++ b/actionmailbox/test/dummy/app/mailers/application_mailer.rb @@ -1,4 +1,4 @@ class ApplicationMailer < ActionMailer::Base - default from: 'from@example.com' - layout 'mailer' + default from: "from@example.com" + layout "mailer" end diff --git a/actionmailbox/test/dummy/app/models/application_record.rb b/actionmailbox/test/dummy/app/models/application_record.rb index 10a4cba84df37..b63caeb8a5c4a 100644 --- a/actionmailbox/test/dummy/app/models/application_record.rb +++ b/actionmailbox/test/dummy/app/models/application_record.rb @@ -1,3 +1,3 @@ class ApplicationRecord < ActiveRecord::Base - self.abstract_class = true + primary_abstract_class end diff --git a/actionmailbox/test/dummy/app/views/layouts/application.html.erb b/actionmailbox/test/dummy/app/views/layouts/application.html.erb index f221f512b7c59..f72b4ef0e7316 100644 --- a/actionmailbox/test/dummy/app/views/layouts/application.html.erb +++ b/actionmailbox/test/dummy/app/views/layouts/application.html.erb @@ -2,10 +2,11 @@ Dummy + <%= csrf_meta_tags %> + <%= csp_meta_tag %> - <%= stylesheet_link_tag 'application', media: 'all' %> - <%= javascript_pack_tag 'application' %> + <%= stylesheet_link_tag "application" %> diff --git a/actionmailbox/test/dummy/app/views/layouts/mailer.html.erb b/actionmailbox/test/dummy/app/views/layouts/mailer.html.erb index cbd34d2e9dd11..3aac9002edca7 100644 --- a/actionmailbox/test/dummy/app/views/layouts/mailer.html.erb +++ b/actionmailbox/test/dummy/app/views/layouts/mailer.html.erb @@ -1,7 +1,7 @@ - + diff --git a/actionmailbox/test/dummy/bin/bundle b/actionmailbox/test/dummy/bin/bundle deleted file mode 100755 index f19acf5b5cc6e..0000000000000 --- a/actionmailbox/test/dummy/bin/bundle +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) -load Gem.bin_path('bundler', 'bundle') diff --git a/actionmailbox/test/dummy/bin/rails b/actionmailbox/test/dummy/bin/rails index 6fb4e4051c481..efc0377492f7e 100755 --- a/actionmailbox/test/dummy/bin/rails +++ b/actionmailbox/test/dummy/bin/rails @@ -1,4 +1,4 @@ #!/usr/bin/env ruby -APP_PATH = File.expand_path('../config/application', __dir__) +APP_PATH = File.expand_path("../config/application", __dir__) require_relative "../config/boot" require "rails/commands" diff --git a/actionmailbox/test/dummy/bin/setup b/actionmailbox/test/dummy/bin/setup index 641207e03c48b..3cd5a9d7801ca 100755 --- a/actionmailbox/test/dummy/bin/setup +++ b/actionmailbox/test/dummy/bin/setup @@ -1,36 +1,33 @@ #!/usr/bin/env ruby require "fileutils" -include FileUtils # path to your application root. -APP_ROOT = File.expand_path('..', __dir__) +APP_ROOT = File.expand_path("..", __dir__) def system!(*args) - system(*args) || abort("\n== Command #{args} failed ==") + system(*args, exception: true) end -chdir APP_ROOT do - # This script is a starting point to set up your application. +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. - puts '== Installing dependencies ==' - system! 'gem install bundler --conservative' - system('bundle check') || system!('bundle install') - - # Install JavaScript dependencies if using Yarn - # system('bin/yarn') + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") # puts "\n== Copying sample files ==" - # unless File.exist?('config/database.yml') - # cp 'config/database.yml.sample', 'config/database.yml' + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" # end puts "\n== Preparing database ==" - system! 'bin/rails db:setup' + system! "bin/rails db:prepare" puts "\n== Removing old logs and tempfiles ==" - system! 'bin/rails log:clear tmp:clear' + system! "bin/rails log:clear tmp:clear" puts "\n== Restarting application server ==" - system! 'bin/rails restart' + system! "bin/rails restart" end diff --git a/actionmailbox/test/dummy/bin/update b/actionmailbox/test/dummy/bin/update deleted file mode 100755 index 610b83448d277..0000000000000 --- a/actionmailbox/test/dummy/bin/update +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env ruby -require "fileutils" -include FileUtils - -# path to your application root. -APP_ROOT = File.expand_path('..', __dir__) - -def system!(*args) - system(*args) || abort("\n== Command #{args} failed ==") -end - -chdir APP_ROOT do - # This script is a way to update your development environment automatically. - # Add necessary update steps to this file. - - puts '== Installing dependencies ==' - system! 'gem install bundler --conservative' - system('bundle check') || system!('bundle install') - - # Install JavaScript dependencies if using Yarn - # system('bin/yarn') - - puts "\n== Updating database ==" - system! 'bin/rails db:migrate' - - puts "\n== Removing old logs and tempfiles ==" - system! 'bin/rails log:clear tmp:clear' - - puts "\n== Restarting application server ==" - system! 'bin/rails restart' -end diff --git a/actionmailbox/test/dummy/bin/yarn b/actionmailbox/test/dummy/bin/yarn deleted file mode 100755 index 460dd565b4a3d..0000000000000 --- a/actionmailbox/test/dummy/bin/yarn +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env ruby -APP_ROOT = File.expand_path('..', __dir__) -Dir.chdir(APP_ROOT) do - begin - exec "yarnpkg", *ARGV - rescue Errno::ENOENT - $stderr.puts "Yarn executable was not detected in the system." - $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" - exit 1 - end -end diff --git a/actionmailbox/test/dummy/config.ru b/actionmailbox/test/dummy/config.ru index 441e6ff0c3cd5..4a3c09a6889a9 100644 --- a/actionmailbox/test/dummy/config.ru +++ b/actionmailbox/test/dummy/config.ru @@ -3,3 +3,4 @@ require_relative "config/environment" run Rails.application +Rails.application.load_server diff --git a/actionmailbox/test/dummy/config/application.rb b/actionmailbox/test/dummy/config/application.rb index 8948a9fa45a2c..8154f557052d9 100644 --- a/actionmailbox/test/dummy/config/application.rb +++ b/actionmailbox/test/dummy/config/application.rb @@ -2,16 +2,26 @@ require "rails/all" +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module Dummy class Application < Rails::Application - # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 6.0 + config.load_defaults Rails::VERSION::STRING.to_f - # Settings in config/environments/* take precedence over those specified here. - # Application configuration can go into files in config/initializers - # -- all .rb files in that directory are automatically loaded after loading - # the framework and any gems in your application. + # For compatibility with applications that use this config + config.action_controller.include_all_helpers = false + + config.active_record.table_name_prefix = 'prefix_' + config.active_record.table_name_suffix = '_suffix' + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") end end diff --git a/actionmailbox/test/dummy/config/boot.rb b/actionmailbox/test/dummy/config/boot.rb index 91e50810ef7f4..d5e0f0fdc80b8 100644 --- a/actionmailbox/test/dummy/config/boot.rb +++ b/actionmailbox/test/dummy/config/boot.rb @@ -1,5 +1,5 @@ # Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../../Gemfile", __dir__) require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) -$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) +$LOAD_PATH.unshift File.expand_path("../../../lib", __dir__) diff --git a/actionmailbox/test/dummy/config/cable.yml b/actionmailbox/test/dummy/config/cable.yml index 1cd0f8363e841..98367f8954247 100644 --- a/actionmailbox/test/dummy/config/cable.yml +++ b/actionmailbox/test/dummy/config/cable.yml @@ -2,7 +2,7 @@ development: adapter: async test: - adapter: async + adapter: test production: adapter: redis diff --git a/actionmailbox/test/dummy/config/database.yml b/actionmailbox/test/dummy/config/database.yml index 0d02f249800d3..796466ba23eed 100644 --- a/actionmailbox/test/dummy/config/database.yml +++ b/actionmailbox/test/dummy/config/database.yml @@ -1,8 +1,8 @@ -# SQLite version 3.x +# SQLite. Versions 3.8.0 and up are supported. # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile -# gem 'sqlite3' +# gem "sqlite3" # default: &default adapter: sqlite3 @@ -11,15 +11,15 @@ default: &default development: <<: *default - database: db/development.sqlite3 + database: storage/development.sqlite3 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default - database: db/test.sqlite3 + database: storage/test.sqlite3 production: <<: *default - database: db/production.sqlite3 + database: storage/production.sqlite3 diff --git a/actionmailbox/test/dummy/config/environments/development.rb b/actionmailbox/test/dummy/config/environments/development.rb index 7a9d9419d7951..4d134bb530348 100644 --- a/actionmailbox/test/dummy/config/environments/development.rb +++ b/actionmailbox/test/dummy/config/environments/development.rb @@ -1,12 +1,12 @@ +require "active_support/core_ext/integer/time" + Rails.application.configure do - # Verifies that versions and hashed value of the package contents in the project's package.json - # config.webpacker.check_yarn_integrity = true # Settings specified here will take precedence over those in config/application.rb. - # In the development environment your application's code is reloaded on - # every request. This slows down response time but is perfect for development + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. - config.cache_classes = false + config.enable_reloading = true # Do not eager load code on boot. config.eager_load = false @@ -14,14 +14,18 @@ # Show full error reports. config.consider_all_requests_local = true + # Enable server timing + config.server_timing = true + # Enable/disable caching. By default caching is disabled. - # Run bin/rails dev:cache to toggle caching. - if Rails.root.join('tmp', 'caching-dev.txt').exist? + # Run rails dev:cache to toggle caching. + if Rails.root.join("tmp/caching-dev.txt").exist? config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{2.days.to_i}" + "Cache-Control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false @@ -29,7 +33,7 @@ config.cache_store = :null_store end - # Store uploaded files on the local file system (see config/storage.yml for options) + # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Don't care if the mailer can't send. @@ -40,27 +44,30 @@ # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true - # Debug mode disables concatenation and preprocessing of assets. - # This option may cause significant delays in view rendering with a large - # number of complex assets. - config.assets.debug = true - # Suppress logger output for asset requests. config.assets.quiet = true - # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true - # Render template filenames as comments in HTML - # config.action_view.annotate_template_file_names = true + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true - # Use an evented file watcher to asynchronously detect changes in source code, - # routes, locales, etc. This feature depends on the listen gem. - # config.file_watcher = ActiveSupport::EventedFileUpdateChecker + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true end diff --git a/actionmailbox/test/dummy/config/environments/production.rb b/actionmailbox/test/dummy/config/environments/production.rb index 33ce2e0186992..999e332a7e2be 100644 --- a/actionmailbox/test/dummy/config/environments/production.rb +++ b/actionmailbox/test/dummy/config/environments/production.rb @@ -1,8 +1,10 @@ +require "active_support/core_ext/integer/time" + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. - config.cache_classes = true + config.enable_reloading = false # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers @@ -18,50 +20,56 @@ # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true - # Disable serving static files from the `/public` folder by default since - # Apache or NGINX already handles this. - config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. + # config.public_file_server.enabled = false - # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass - # Do not fallback to assets pipeline if a precompiled asset is missed. + # Do not fall back to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = 'http://assets.example.com' + # config.asset_host = "http://assets.example.com" # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX - # Store uploaded files on the local file system (see config/storage.yml for options) + # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local - # Mount Action Cable outside main process or domain + # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil - # config.action_cable.url = 'wss://example.com/cable' - # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + # config.action_cable.url = "wss://example.com/cable" + # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. + # config.assume_ssl = true # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true - # Use the lowest log level to ensure availability of diagnostic information - # when problems arise. - config.log_level = :debug + # Log to STDOUT by default + config.logger = ActiveSupport::Logger.new(STDOUT) + .tap { |logger| logger.formatter = ::Logger::Formatter.new } + .then { |logger| ActiveSupport::TaggedLogging.new(logger) } # Prepend all log lines with the following tags. config.log_tags = [ :request_id ] + # "info" includes generic and useful information about system operation, but avoids logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). Use "debug" + # for everything. + config.log_level = ENV.fetch("RAILS_LOG_LEVEL") { "info" } + # Use a different cache store in production. # config.cache_store = :mem_cache_store - # Use a real queuing backend for Active Job (and separate queues per environment) + # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque - # config.active_job.queue_name_prefix = "dummy_#{Rails.env}" + # config.active_job.queue_name_prefix = "dummy_production" config.action_mailer.perform_caching = false @@ -73,28 +81,9 @@ # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true - # Send deprecation notices to registered listeners. - config.active_support.deprecation = :notify - - # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new - - # Use a different logger for distributed setups. - # require "syslog/logger" - # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') - - if ENV["RAILS_LOG_TO_STDOUT"].present? - logger = ActiveSupport::Logger.new(STDOUT) - logger.formatter = config.log_formatter - config.logger = ActiveSupport::TaggedLogging.new(logger) - end + # Don't log any deprecations. + config.active_support.report_deprecations = false # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false - - # Prepare the ingress controller used to receive mail - # config.action_mailbox.ingress = :postfix - - # Verifies that versions and hashed value of the package contents in the project's package.json - config.webpacker.check_yarn_integrity = false end diff --git a/actionmailbox/test/dummy/config/environments/test.rb b/actionmailbox/test/dummy/config/environments/test.rb index 6c2cd6f8ed54e..5b1b89421f004 100644 --- a/actionmailbox/test/dummy/config/environments/test.rb +++ b/actionmailbox/test/dummy/config/environments/test.rb @@ -1,34 +1,39 @@ +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! - config.cache_classes = true + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false - # Do not eager load code on boot. This avoids loading your whole application - # just for the purpose of running a single test. If you are using a tool that - # preloads Rails for running tests, you may have to set it to true. - config.eager_load = false + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? # Configure public file server for tests with Cache-Control for performance. - config.public_file_server.enabled = true config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + "Cache-Control" => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false + config.cache_store = :null_store # Raise exceptions instead of rendering exception templates. - config.action_dispatch.show_exceptions = false + config.action_dispatch.show_exceptions = :rescuable # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false - # Store uploaded files on the local file system in a temporary directory + # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test config.action_mailer.perform_caching = false @@ -41,9 +46,18 @@ # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true - # Render template filenames as comments in HTML - # config.action_view.annotate_template_file_names = true + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true end diff --git a/actionmailbox/test/dummy/config/initializers/application_controller_renderer.rb b/actionmailbox/test/dummy/config/initializers/application_controller_renderer.rb deleted file mode 100644 index 89d2efab2ba65..0000000000000 --- a/actionmailbox/test/dummy/config/initializers/application_controller_renderer.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# ActiveSupport::Reloader.to_prepare do -# ApplicationController.renderer.defaults.merge!( -# http_host: 'example.org', -# https: false -# ) -# end diff --git a/actionmailbox/test/dummy/config/initializers/assets.rb b/actionmailbox/test/dummy/config/initializers/assets.rb index 4b828e80cb778..2eeef966fe872 100644 --- a/actionmailbox/test/dummy/config/initializers/assets.rb +++ b/actionmailbox/test/dummy/config/initializers/assets.rb @@ -1,12 +1,10 @@ # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = '1.0' +Rails.application.config.assets.version = "1.0" # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path -# Add Yarn node_modules folder to the asset load path. -Rails.application.config.assets.paths << Rails.root.join('node_modules') # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets diff --git a/actionmailbox/test/dummy/config/initializers/backtrace_silencers.rb b/actionmailbox/test/dummy/config/initializers/backtrace_silencers.rb deleted file mode 100644 index 3c56b21b3ca27..0000000000000 --- a/actionmailbox/test/dummy/config/initializers/backtrace_silencers.rb +++ /dev/null @@ -1,7 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. -# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } - -# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. -# Rails.backtrace_cleaner.remove_silencers! diff --git a/actionmailbox/test/dummy/config/initializers/content_security_policy.rb b/actionmailbox/test/dummy/config/initializers/content_security_policy.rb index edde7f42b88f7..b3076b38fe143 100644 --- a/actionmailbox/test/dummy/config/initializers/content_security_policy.rb +++ b/actionmailbox/test/dummy/config/initializers/content_security_policy.rb @@ -1,22 +1,25 @@ # Be sure to restart your server when you modify this file. -# Define an application-wide content security policy -# For further information see the following documentation -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header -# Rails.application.config.content_security_policy do |policy| -# policy.default_src :self, :https -# policy.font_src :self, :https, :data -# policy.img_src :self, :https, :data -# policy.object_src :none -# policy.script_src :self, :https -# policy.style_src :self, :https, :unsafe_inline - -# # Specify URI for violation reports -# # policy.report_uri "/csp-violation-report-endpoint" +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true # end - -# Report CSP violations to a specified URI -# For further information see the following documentation: -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only -# Rails.application.config.content_security_policy_report_only = true diff --git a/actionmailbox/test/dummy/config/initializers/cookies_serializer.rb b/actionmailbox/test/dummy/config/initializers/cookies_serializer.rb deleted file mode 100644 index 5a6a32d371fe5..0000000000000 --- a/actionmailbox/test/dummy/config/initializers/cookies_serializer.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Specify a serializer for the signed and encrypted cookie jars. -# Valid options are :json, :marshal, and :hybrid. -Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/actionmailbox/test/dummy/config/initializers/filter_parameter_logging.rb b/actionmailbox/test/dummy/config/initializers/filter_parameter_logging.rb index 4a994e1e7bb7c..adc6568ce8372 100644 --- a/actionmailbox/test/dummy/config/initializers/filter_parameter_logging.rb +++ b/actionmailbox/test/dummy/config/initializers/filter_parameter_logging.rb @@ -1,4 +1,8 @@ # Be sure to restart your server when you modify this file. -# Configure sensitive parameters which will be filtered from the log file. -Rails.application.config.filter_parameters += [:password] +# Configure parameters to be filtered from the log file. Use this to limit dissemination of +# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported +# notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/actionmailbox/test/dummy/config/initializers/inflections.rb b/actionmailbox/test/dummy/config/initializers/inflections.rb index ac033bf9dc846..3860f659ead02 100644 --- a/actionmailbox/test/dummy/config/initializers/inflections.rb +++ b/actionmailbox/test/dummy/config/initializers/inflections.rb @@ -4,13 +4,13 @@ # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.plural /^(ox)$/i, '\1en' -# inflect.singular /^(ox)en/i, '\1' -# inflect.irregular 'person', 'people' +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym 'RESTful' +# inflect.acronym "RESTful" # end diff --git a/actionmailbox/test/dummy/config/initializers/mime_types.rb b/actionmailbox/test/dummy/config/initializers/mime_types.rb deleted file mode 100644 index dc1899682b01c..0000000000000 --- a/actionmailbox/test/dummy/config/initializers/mime_types.rb +++ /dev/null @@ -1,4 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Add new mime types for use in respond_to blocks: -# Mime::Type.register "text/richtext", :rtf diff --git a/actionmailbox/test/dummy/config/initializers/secret_token.rb b/actionmailbox/test/dummy/config/initializers/secret_token.rb deleted file mode 100644 index ada68ba8a2507..0000000000000 --- a/actionmailbox/test/dummy/config/initializers/secret_token.rb +++ /dev/null @@ -1 +0,0 @@ -Rails.application.config.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" diff --git a/actionmailbox/test/dummy/config/initializers/wrap_parameters.rb b/actionmailbox/test/dummy/config/initializers/wrap_parameters.rb deleted file mode 100644 index bbfc3961bffef..0000000000000 --- a/actionmailbox/test/dummy/config/initializers/wrap_parameters.rb +++ /dev/null @@ -1,14 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# This file contains settings for ActionController::ParamsWrapper which -# is enabled by default. - -# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. -ActiveSupport.on_load(:action_controller) do - wrap_parameters format: [:json] -end - -# To enable root element in JSON for ActiveRecord objects. -# ActiveSupport.on_load(:active_record) do -# self.include_root_in_json = true -# end diff --git a/actionmailbox/test/dummy/config/locales/en.yml b/actionmailbox/test/dummy/config/locales/en.yml index cf9b342d0aebf..6c349ae5e3743 100644 --- a/actionmailbox/test/dummy/config/locales/en.yml +++ b/actionmailbox/test/dummy/config/locales/en.yml @@ -1,14 +1,14 @@ -# Files in the config/locales directory are used for internationalization -# and are automatically loaded by Rails. If you want to use locales other -# than English, add the necessary files in this directory. +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # -# I18n.t 'hello' +# I18n.t "hello" # # In views, this is aliased to just `t`: # -# <%= t('hello') %> +# <%= t("hello") %> # # To use a different locale, set it with `I18n.locale`: # @@ -16,18 +16,16 @@ # # This would use the information in config/locales/es.yml. # -# The following keys must be escaped otherwise they will not be retrieved by -# the default I18n backend: +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. # -# true, false, on, off, yes, no +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: # -# Instead, surround them with single quotes. -# -# en: -# 'true': 'foo' -# -# To learn more, please read the Rails Internationalization guide -# available at https://guides.rubyonrails.org/i18n.html. +# en: +# "yes": yup +# enabled: "ON" en: hello: "Hello world" diff --git a/actionmailbox/test/dummy/config/puma.rb b/actionmailbox/test/dummy/config/puma.rb index d4e7c2492f4ba..09a5c4b7865e7 100644 --- a/actionmailbox/test/dummy/config/puma.rb +++ b/actionmailbox/test/dummy/config/puma.rb @@ -1,34 +1,38 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. + # Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. -# -threads_count = Integer(ENV.fetch("RAILS_MAX_THREADS", "5")) -threads threads_count, threads_count +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies that the worker count should equal the number of processors in production. +if ENV["RAILS_ENV"] == "production" + worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }) + workers worker_count if worker_count > 1 +end + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" # Specifies the `port` that Puma will listen on to receive requests; default is 3000. -# -port Integer(ENV.fetch("PORT", "3000")) +port ENV.fetch("PORT") { 3000 } # Specifies the `environment` that Puma will run in. -# -environment ENV.fetch("RAILS_ENV", "development") - -# Specifies the number of `workers` to boot in clustered mode. -# Workers are forked web server processes. If using threads and workers together -# the concurrency of the application would be max `threads` * `workers`. -# Workers do not work on JRuby or Windows (both of which do not support -# processes). -# -# workers Integer(ENV.fetch("WEB_CONCURRENCY", "2")) +environment ENV.fetch("RAILS_ENV") { "development" } -# Use the `preload_app!` method when specifying a `workers` number. -# This directive tells Puma to first boot the application and load code -# before forking the application. This takes advantage of Copy On Write -# process behavior so workers use less memory. -# -# preload_app! +# Specifies the `pidfile` that Puma will use. +if ENV["PIDFILE"] + pidfile ENV["PIDFILE"] +else + pidfile "tmp/pids/server.pid" if ENV.fetch("RAILS_ENV", "development") == "development" +end # Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart diff --git a/actionmailbox/test/dummy/config/routes.rb b/actionmailbox/test/dummy/config/routes.rb index 1fc667e2425de..1daf9a4121a8b 100644 --- a/actionmailbox/test/dummy/config/routes.rb +++ b/actionmailbox/test/dummy/config/routes.rb @@ -1,4 +1,2 @@ Rails.application.routes.draw do - resources :messages - # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html end diff --git a/actionmailbox/test/dummy/config/spring.rb b/actionmailbox/test/dummy/config/spring.rb deleted file mode 100644 index 9fa7863f99d0d..0000000000000 --- a/actionmailbox/test/dummy/config/spring.rb +++ /dev/null @@ -1,6 +0,0 @@ -%w[ - .ruby-version - .rbenv-vars - tmp/restart.txt - tmp/caching-dev.txt -].each { |path| Spring.watch(path) } diff --git a/actionmailbox/test/dummy/config/storage.yml b/actionmailbox/test/dummy/config/storage.yml index d8e4267cc4789..c26dd89d229af 100644 --- a/actionmailbox/test/dummy/config/storage.yml +++ b/actionmailbox/test/dummy/config/storage.yml @@ -6,28 +6,31 @@ local: service: Disk root: <%= Rails.root.join("storage") %> +test_email: + service: Disk + root: <%= Rails.root.join("tmp/storage_email") %> + # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: # service: S3 # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> # region: us-east-1 -# bucket: your_own_bucket +# bucket: your_own_bucket-<%= Rails.env %> # Remember not to checkin your GCS keyfile to a repository # google: # service: GCS # project: your_project # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> -# bucket: your_own_bucket +# bucket: your_own_bucket-<%= Rails.env %> # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) # microsoft: # service: AzureStorage -# path: your_azure_storage_path # storage_account_name: your_account_name # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> -# container: your_container_name +# container: your_container_name-<%= Rails.env %> # mirror: # service: Mirror diff --git a/actionmailbox/test/dummy/config/webpack/development.js b/actionmailbox/test/dummy/config/webpack/development.js deleted file mode 100644 index 81269f6513de6..0000000000000 --- a/actionmailbox/test/dummy/config/webpack/development.js +++ /dev/null @@ -1,3 +0,0 @@ -const environment = require('./environment') - -module.exports = environment.toWebpackConfig() diff --git a/actionmailbox/test/dummy/config/webpack/environment.js b/actionmailbox/test/dummy/config/webpack/environment.js deleted file mode 100644 index d16d9af7434fa..0000000000000 --- a/actionmailbox/test/dummy/config/webpack/environment.js +++ /dev/null @@ -1,3 +0,0 @@ -const { environment } = require('@rails/webpacker') - -module.exports = environment diff --git a/actionmailbox/test/dummy/config/webpack/production.js b/actionmailbox/test/dummy/config/webpack/production.js deleted file mode 100644 index 81269f6513de6..0000000000000 --- a/actionmailbox/test/dummy/config/webpack/production.js +++ /dev/null @@ -1,3 +0,0 @@ -const environment = require('./environment') - -module.exports = environment.toWebpackConfig() diff --git a/actionmailbox/test/dummy/config/webpack/test.js b/actionmailbox/test/dummy/config/webpack/test.js deleted file mode 100644 index 81269f6513de6..0000000000000 --- a/actionmailbox/test/dummy/config/webpack/test.js +++ /dev/null @@ -1,3 +0,0 @@ -const environment = require('./environment') - -module.exports = environment.toWebpackConfig() diff --git a/actionmailbox/test/dummy/config/webpacker.yml b/actionmailbox/test/dummy/config/webpacker.yml deleted file mode 100644 index d3f24e1b4bf3c..0000000000000 --- a/actionmailbox/test/dummy/config/webpacker.yml +++ /dev/null @@ -1,65 +0,0 @@ -# Note: You must restart bin/webpack-dev-server for changes to take effect - -default: &default - source_path: app/javascript - source_entry_path: packs - public_output_path: packs - cache_path: tmp/cache/webpacker - - # Additional paths webpack should lookup modules - # ['app/assets', 'engine/foo/app/assets'] - resolved_paths: [] - - # Reload manifest.json on all requests so we reload latest compiled packs - cache_manifest: false - - extensions: - - .js - - .sass - - .scss - - .css - - .png - - .svg - - .gif - - .jpeg - - .jpg - -development: - <<: *default - compile: true - - # Reference: https://webpack.js.org/configuration/dev-server/ - dev_server: - https: false - host: localhost - port: 3035 - public: localhost:3035 - hmr: false - # Inline should be set to true if using HMR - inline: true - overlay: true - compress: true - disable_host_check: true - use_local_ip: false - quiet: false - headers: - 'Access-Control-Allow-Origin': '*' - watch_options: - ignored: /node_modules/ - - -test: - <<: *default - compile: true - - # Compile test packs to a separate directory - public_output_path: packs-test - -production: - <<: *default - - # Production depends on precompilation of packs prior to booting for performance. - compile: false - - # Cache manifest.json for performance - cache_manifest: true diff --git a/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb b/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb index 2bf4335808976..2a1e2f7a8e2e1 100644 --- a/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb +++ b/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb @@ -1,6 +1,6 @@ class CreateActionMailboxTables < ActiveRecord::Migration[6.0] def change - create_table :action_mailbox_inbound_emails do |t| + create_table :action_mailbox_inbound_emails, id: primary_key_type do |t| t.integer :status, default: 0, null: false t.string :message_id, null: false t.string :message_checksum, null: false @@ -10,4 +10,10 @@ def change t.index [ :message_id, :message_checksum ], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true end end + + private + def primary_key_type + config = Rails.configuration.generators + config.options[config.orm][:primary_key_type] || :primary_key + end end diff --git a/actionmailbox/test/dummy/db/schema.rb b/actionmailbox/test/dummy/db/schema.rb index c2935f0ac38f8..acbc0de9d3715 100644 --- a/actionmailbox/test/dummy/db/schema.rb +++ b/actionmailbox/test/dummy/db/schema.rb @@ -10,14 +10,13 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2018_02_12_164506) do - +ActiveRecord::Schema[8.1].define(version: 2018_02_12_164506) do create_table "action_mailbox_inbound_emails", force: :cascade do |t| t.integer "status", default: 0, null: false t.string "message_id", null: false t.string "message_checksum", null: false - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["message_id", "message_checksum"], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true end @@ -26,7 +25,7 @@ t.string "record_type", null: false t.integer "record_id", null: false t.integer "blob_id", null: false - t.datetime "created_at", null: false + t.datetime "created_at", precision: nil, null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end @@ -39,9 +38,16 @@ t.string "service_name", null: false t.bigint "byte_size", null: false t.string "checksum", null: false - t.datetime "created_at", null: false + t.datetime "created_at", precision: nil, null: false t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end + create_table "active_storage_variant_records", force: :cascade do |t| + t.integer "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true + end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" end diff --git a/actionmailbox/test/dummy/package.json b/actionmailbox/test/dummy/package.json deleted file mode 100644 index d1f979fcaeb25..0000000000000 --- a/actionmailbox/test/dummy/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "dummy", - "private": true, - "dependencies": { - "@rails/webpacker": "^4.0.2", - "activetext": "file:../.." - }, - "devDependencies": { - "webpack-dev-server": "^3.2.1" - } -} diff --git a/actionmailbox/test/dummy/public/400.html b/actionmailbox/test/dummy/public/400.html new file mode 100644 index 0000000000000..f59c79ab82f05 --- /dev/null +++ b/actionmailbox/test/dummy/public/400.html @@ -0,0 +1,114 @@ + + + + + + + The server cannot process the request due to a client error (400 Bad Request) + + + + + + + + + + + + + +
+
+ +
+
+

The server cannot process the request due to a client error. Please check the request and try again. If you're the application owner check the logs for more information.

+
+
+ + + + diff --git a/actionmailbox/test/dummy/public/404.html b/actionmailbox/test/dummy/public/404.html index 2be3af26fc5a3..26d16027c6a4c 100644 --- a/actionmailbox/test/dummy/public/404.html +++ b/actionmailbox/test/dummy/public/404.html @@ -1,67 +1,114 @@ - - - - The page you were looking for doesn't exist (404) - - - - - - -
-
-

The page you were looking for doesn't exist.

-

You may have mistyped the address or the page may have moved.

-
-

If you are the application owner check the logs for more information.

-
- + + + + + + + The page you were looking for doesn't exist (404 Not found) + + + + + + + + + + + + + +
+
+ +
+
+

The page you were looking for doesn't exist. You may have mistyped the address or the page may have moved. If you're the application owner check the logs for more information.

+
+
+ + + diff --git a/actionmailbox/test/dummy/public/422.html b/actionmailbox/test/dummy/public/422.html index c08eac0d1df79..ed5a5805d0e5f 100644 --- a/actionmailbox/test/dummy/public/422.html +++ b/actionmailbox/test/dummy/public/422.html @@ -1,67 +1,114 @@ - - - - The change you wanted was rejected (422) - - - - - - -
-
-

The change you wanted was rejected.

-

Maybe you tried to change something you didn't have access to.

-
-

If you are the application owner check the logs for more information.

-
- + + + + + + + The change you wanted was rejected (422 Unprocessable Entity) + + + + + + + + + + + + + +
+
+ +
+
+

The change you wanted was rejected. Maybe you tried to change something you didn't have access to. If you're the application owner check the logs for more information.

+
+
+ + + diff --git a/actionmailbox/test/dummy/public/500.html b/actionmailbox/test/dummy/public/500.html index 78a030af22ea1..318723853a010 100644 --- a/actionmailbox/test/dummy/public/500.html +++ b/actionmailbox/test/dummy/public/500.html @@ -1,66 +1,114 @@ - - - - We're sorry, but something went wrong (500) - - - - - - -
-
-

We're sorry, but something went wrong.

-
-

If you are the application owner check the logs for more information.

-
- + + + + + + + We're sorry, but something went wrong (500 Internal Server Error) + + + + + + + + + + + + + +
+
+ +
+
+

We're sorry, but something went wrong.
If you're the application owner check the logs for more information.

+
+
+ + + diff --git a/actionmailbox/test/dummy/yarn.lock b/actionmailbox/test/dummy/yarn.lock deleted file mode 100644 index ba7eb2c28ea37..0000000000000 --- a/actionmailbox/test/dummy/yarn.lock +++ /dev/null @@ -1,7578 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" - integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA== - dependencies: - "@babel/highlight" "^7.0.0" - -"@babel/core@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.4.tgz#921a5a13746c21e32445bf0798680e9d11a6530b" - integrity sha512-jRsuseXBo9pN197KnDwhhaaBzyZr2oIcLHHTt2oDdQrej5Qp57dCCJafWx5ivU8/alEYDpssYqv1MUqcxwQlrA== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.3.4" - "@babel/helpers" "^7.2.0" - "@babel/parser" "^7.3.4" - "@babel/template" "^7.2.2" - "@babel/traverse" "^7.3.4" - "@babel/types" "^7.3.4" - convert-source-map "^1.1.0" - debug "^4.1.0" - json5 "^2.1.0" - lodash "^4.17.11" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - -"@babel/generator@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.4.tgz#9aa48c1989257877a9d971296e5b73bfe72e446e" - integrity sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg== - dependencies: - "@babel/types" "^7.3.4" - jsesc "^2.5.1" - lodash "^4.17.11" - source-map "^0.5.0" - trim-right "^1.0.1" - -"@babel/helper-annotate-as-pure@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" - integrity sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q== - dependencies: - "@babel/types" "^7.0.0" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz#6b69628dfe4087798e0c4ed98e3d4a6b2fbd2f5f" - integrity sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.1.0" - "@babel/types" "^7.0.0" - -"@babel/helper-call-delegate@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.1.0.tgz#6a957f105f37755e8645343d3038a22e1449cc4a" - integrity sha512-YEtYZrw3GUK6emQHKthltKNZwszBcHK58Ygcis+gVUrF4/FmTVr5CCqQNSfmvg2y+YDEANyYoaLz/SHsnusCwQ== - dependencies: - "@babel/helper-hoist-variables" "^7.0.0" - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.0.0" - -"@babel/helper-create-class-features-plugin@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.4.tgz#092711a7a3ad8ea34de3e541644c2ce6af1f6f0c" - integrity sha512-uFpzw6L2omjibjxa8VGZsJUPL5wJH0zzGKpoz0ccBkzIa6C8kWNUbiBmQ0rgOKWlHJ6qzmfa6lTiGchiV8SC+g== - dependencies: - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-member-expression-to-functions" "^7.0.0" - "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.3.4" - "@babel/helper-split-export-declaration" "^7.0.0" - -"@babel/helper-define-map@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz#3b74caec329b3c80c116290887c0dd9ae468c20c" - integrity sha512-yPPcW8dc3gZLN+U1mhYV91QU3n5uTbx7DUdf8NnPbjS0RMwBuHi9Xt2MUgppmNz7CJxTBWsGczTiEp1CSOTPRg== - dependencies: - "@babel/helper-function-name" "^7.1.0" - "@babel/types" "^7.0.0" - lodash "^4.17.10" - -"@babel/helper-explode-assignable-expression@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz#537fa13f6f1674df745b0c00ec8fe4e99681c8f6" - integrity sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA== - dependencies: - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.0.0" - -"@babel/helper-function-name@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53" - integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw== - dependencies: - "@babel/helper-get-function-arity" "^7.0.0" - "@babel/template" "^7.1.0" - "@babel/types" "^7.0.0" - -"@babel/helper-get-function-arity@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3" - integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ== - dependencies: - "@babel/types" "^7.0.0" - -"@babel/helper-hoist-variables@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0.tgz#46adc4c5e758645ae7a45deb92bab0918c23bb88" - integrity sha512-Ggv5sldXUeSKsuzLkddtyhyHe2YantsxWKNi7A+7LeD12ExRDWTRk29JCXpaHPAbMaIPZSil7n+lq78WY2VY7w== - dependencies: - "@babel/types" "^7.0.0" - -"@babel/helper-member-expression-to-functions@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz#8cd14b0a0df7ff00f009e7d7a436945f47c7a16f" - integrity sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg== - dependencies: - "@babel/types" "^7.0.0" - -"@babel/helper-module-imports@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d" - integrity sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A== - dependencies: - "@babel/types" "^7.0.0" - -"@babel/helper-module-transforms@^7.1.0": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.2.2.tgz#ab2f8e8d231409f8370c883d20c335190284b963" - integrity sha512-YRD7I6Wsv+IHuTPkAmAS4HhY0dkPobgLftHp0cRGZSdrRvmZY8rFvae/GVu3bD00qscuvK3WPHB3YdNpBXUqrA== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/helper-simple-access" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/template" "^7.2.2" - "@babel/types" "^7.2.2" - lodash "^4.17.10" - -"@babel/helper-optimise-call-expression@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz#a2920c5702b073c15de51106200aa8cad20497d5" - integrity sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g== - dependencies: - "@babel/types" "^7.0.0" - -"@babel/helper-plugin-utils@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" - integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== - -"@babel/helper-regex@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.0.0.tgz#2c1718923b57f9bbe64705ffe5640ac64d9bdb27" - integrity sha512-TR0/N0NDCcUIUEbqV6dCO+LptmmSQFQ7q70lfcEB4URsjD0E1HzicrwUH+ap6BAQ2jhCX9Q4UqZy4wilujWlkg== - dependencies: - lodash "^4.17.10" - -"@babel/helper-remap-async-to-generator@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz#361d80821b6f38da75bd3f0785ece20a88c5fe7f" - integrity sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.0.0" - "@babel/helper-wrap-function" "^7.1.0" - "@babel/template" "^7.1.0" - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.0.0" - -"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.3.4.tgz#a795208e9b911a6eeb08e5891faacf06e7013e13" - integrity sha512-pvObL9WVf2ADs+ePg0jrqlhHoxRXlOa+SHRHzAXIz2xkYuOHfGl+fKxPMaS4Fq+uje8JQPobnertBBvyrWnQ1A== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.0.0" - "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/traverse" "^7.3.4" - "@babel/types" "^7.3.4" - -"@babel/helper-simple-access@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz#65eeb954c8c245beaa4e859da6188f39d71e585c" - integrity sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w== - dependencies: - "@babel/template" "^7.1.0" - "@babel/types" "^7.0.0" - -"@babel/helper-split-export-declaration@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz#3aae285c0311c2ab095d997b8c9a94cad547d813" - integrity sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag== - dependencies: - "@babel/types" "^7.0.0" - -"@babel/helper-wrap-function@^7.1.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa" - integrity sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ== - dependencies: - "@babel/helper-function-name" "^7.1.0" - "@babel/template" "^7.1.0" - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.2.0" - -"@babel/helpers@^7.2.0": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.3.1.tgz#949eec9ea4b45d3210feb7dc1c22db664c9e44b9" - integrity sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA== - dependencies: - "@babel/template" "^7.1.2" - "@babel/traverse" "^7.1.5" - "@babel/types" "^7.3.0" - -"@babel/highlight@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" - integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw== - dependencies: - chalk "^2.0.0" - esutils "^2.0.2" - js-tokens "^4.0.0" - -"@babel/parser@^7.2.2", "@babel/parser@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c" - integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ== - -"@babel/plugin-proposal-async-generator-functions@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" - integrity sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-remap-async-to-generator" "^7.1.0" - "@babel/plugin-syntax-async-generators" "^7.2.0" - -"@babel/plugin-proposal-class-properties@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.4.tgz#410f5173b3dc45939f9ab30ca26684d72901405e" - integrity sha512-lUf8D3HLs4yYlAo8zjuneLvfxN7qfKv1Yzbj5vjqaqMJxgJA3Ipwp4VUJ+OrOdz53Wbww6ahwB8UhB2HQyLotA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.3.4" - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-proposal-json-strings@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317" - integrity sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-json-strings" "^7.2.0" - -"@babel/plugin-proposal-object-rest-spread@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.4.tgz#47f73cf7f2a721aad5c0261205405c642e424654" - integrity sha512-j7VQmbbkA+qrzNqbKHrBsW3ddFnOeva6wzSe/zB7T+xaxGc+RCpwo44wCmRixAIGRoIpmVgvzFzNJqQcO3/9RA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-object-rest-spread" "^7.2.0" - -"@babel/plugin-proposal-optional-catch-binding@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz#135d81edb68a081e55e56ec48541ece8065c38f5" - integrity sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" - -"@babel/plugin-proposal-unicode-property-regex@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.2.0.tgz#abe7281fe46c95ddc143a65e5358647792039520" - integrity sha512-LvRVYb7kikuOtIoUeWTkOxQEV1kYvL5B6U3iWEGCzPNRus1MzJweFqORTj+0jkxozkTSYNJozPOddxmqdqsRpw== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.0.0" - regexpu-core "^4.2.0" - -"@babel/plugin-syntax-async-generators@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz#69e1f0db34c6f5a0cf7e2b3323bf159a76c8cb7f" - integrity sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-syntax-dynamic-import@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612" - integrity sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-syntax-json-strings@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470" - integrity sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-syntax-object-rest-spread@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e" - integrity sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz#a94013d6eda8908dfe6a477e7f9eda85656ecf5c" - integrity sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-arrow-functions@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550" - integrity sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-async-to-generator@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.3.4.tgz#4e45408d3c3da231c0e7b823f407a53a7eb3048c" - integrity sha512-Y7nCzv2fw/jEZ9f678MuKdMo99MFDJMT/PvD9LisrR5JDFcJH6vYeH6RnjVt3p5tceyGRvTtEN0VOlU+rgHZjA== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-remap-async-to-generator" "^7.1.0" - -"@babel/plugin-transform-block-scoped-functions@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz#5d3cc11e8d5ddd752aa64c9148d0db6cb79fd190" - integrity sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-block-scoping@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.3.4.tgz#5c22c339de234076eee96c8783b2fed61202c5c4" - integrity sha512-blRr2O8IOZLAOJklXLV4WhcEzpYafYQKSGT3+R26lWG41u/FODJuBggehtOwilVAcFu393v3OFj+HmaE6tVjhA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - lodash "^4.17.11" - -"@babel/plugin-transform-classes@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.3.4.tgz#dc173cb999c6c5297e0b5f2277fdaaec3739d0cc" - integrity sha512-J9fAvCFBkXEvBimgYxCjvaVDzL6thk0j0dBvCeZmIUDBwyt+nv6HfbImsSrWsYXfDNDivyANgJlFXDUWRTZBuA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.0.0" - "@babel/helper-define-map" "^7.1.0" - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.3.4" - "@babel/helper-split-export-declaration" "^7.0.0" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz#83a7df6a658865b1c8f641d510c6f3af220216da" - integrity sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-destructuring@^7.2.0", "@babel/plugin-transform-destructuring@^7.3.2": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.3.2.tgz#f2f5520be055ba1c38c41c0e094d8a461dd78f2d" - integrity sha512-Lrj/u53Ufqxl/sGxyjsJ2XNtNuEjDyjpqdhMNh5aZ+XFOdThL46KBj27Uem4ggoezSYBxKWAil6Hu8HtwqesYw== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-dotall-regex@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.2.0.tgz#f0aabb93d120a8ac61e925ea0ba440812dbe0e49" - integrity sha512-sKxnyHfizweTgKZf7XsXu/CNupKhzijptfTM+bozonIuyVrLWVUvYjE2bhuSBML8VQeMxq4Mm63Q9qvcvUcciQ== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.0.0" - regexpu-core "^4.1.3" - -"@babel/plugin-transform-duplicate-keys@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.2.0.tgz#d952c4930f312a4dbfff18f0b2914e60c35530b3" - integrity sha512-q+yuxW4DsTjNceUiTzK0L+AfQ0zD9rWaTLiUqHA8p0gxx7lu1EylenfzjeIWNkPy6e/0VG/Wjw9uf9LueQwLOw== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-exponentiation-operator@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz#a63868289e5b4007f7054d46491af51435766008" - integrity sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0" - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-for-of@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.2.0.tgz#ab7468befa80f764bb03d3cb5eef8cc998e1cad9" - integrity sha512-Kz7Mt0SsV2tQk6jG5bBv5phVbkd0gd27SgYD4hH1aLMJRchM0dzHaXvrWhVZ+WxAlDoAKZ7Uy3jVTW2mKXQ1WQ== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-function-name@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.2.0.tgz#f7930362829ff99a3174c39f0afcc024ef59731a" - integrity sha512-kWgksow9lHdvBC2Z4mxTsvc7YdY7w/V6B2vy9cTIPtLEE9NhwoWivaxdNM/S37elu5bqlLP/qOY906LukO9lkQ== - dependencies: - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-literals@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz#690353e81f9267dad4fd8cfd77eafa86aba53ea1" - integrity sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-modules-amd@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.2.0.tgz#82a9bce45b95441f617a24011dc89d12da7f4ee6" - integrity sha512-mK2A8ucqz1qhrdqjS9VMIDfIvvT2thrEsIQzbaTdc5QFzhDjQv2CkJJ5f6BXIkgbmaoax3zBr2RyvV/8zeoUZw== - dependencies: - "@babel/helper-module-transforms" "^7.1.0" - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-modules-commonjs@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.2.0.tgz#c4f1933f5991d5145e9cfad1dfd848ea1727f404" - integrity sha512-V6y0uaUQrQPXUrmj+hgnks8va2L0zcZymeU7TtWEgdRLNkceafKXEduv7QzgQAE4lT+suwooG9dC7LFhdRAbVQ== - dependencies: - "@babel/helper-module-transforms" "^7.1.0" - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-simple-access" "^7.1.0" - -"@babel/plugin-transform-modules-systemjs@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.3.4.tgz#813b34cd9acb6ba70a84939f3680be0eb2e58861" - integrity sha512-VZ4+jlGOF36S7TjKs8g4ojp4MEI+ebCQZdswWb/T9I4X84j8OtFAyjXjt/M16iIm5RIZn0UMQgg/VgIwo/87vw== - dependencies: - "@babel/helper-hoist-variables" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-modules-umd@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz#7678ce75169f0877b8eb2235538c074268dd01ae" - integrity sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw== - dependencies: - "@babel/helper-module-transforms" "^7.1.0" - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.3.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.3.0.tgz#140b52985b2d6ef0cb092ef3b29502b990f9cd50" - integrity sha512-NxIoNVhk9ZxS+9lSoAQ/LM0V2UEvARLttEHUrRDGKFaAxOYQcrkN/nLRE+BbbicCAvZPl7wMP0X60HsHE5DtQw== - dependencies: - regexp-tree "^0.1.0" - -"@babel/plugin-transform-new-target@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz#ae8fbd89517fa7892d20e6564e641e8770c3aa4a" - integrity sha512-yin069FYjah+LbqfGeTfzIBODex/e++Yfa0rH0fpfam9uTbuEeEOx5GLGr210ggOV77mVRNoeqSYqeuaqSzVSw== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-object-super@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz#b35d4c10f56bab5d650047dad0f1d8e8814b6598" - integrity sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.1.0" - -"@babel/plugin-transform-parameters@^7.2.0": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.3.3.tgz#3a873e07114e1a5bee17d04815662c8317f10e30" - integrity sha512-IrIP25VvXWu/VlBWTpsjGptpomtIkYrN/3aDp4UKm7xK6UxZY88kcJ1UwETbzHAlwN21MnNfwlar0u8y3KpiXw== - dependencies: - "@babel/helper-call-delegate" "^7.1.0" - "@babel/helper-get-function-arity" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-regenerator@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.3.4.tgz#1601655c362f5b38eead6a52631f5106b29fa46a" - integrity sha512-hvJg8EReQvXT6G9H2MvNPXkv9zK36Vxa1+csAVTpE1J3j0zlHplw76uudEbJxgvqZzAq9Yh45FLD4pk5mKRFQA== - dependencies: - regenerator-transform "^0.13.4" - -"@babel/plugin-transform-runtime@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.3.4.tgz#57805ac8c1798d102ecd75c03b024a5b3ea9b431" - integrity sha512-PaoARuztAdd5MgeVjAxnIDAIUet5KpogqaefQvPOmPYCxYoaPhautxDh3aO8a4xHsKgT/b9gSxR0BKK1MIewPA== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" - resolve "^1.8.1" - semver "^5.5.1" - -"@babel/plugin-transform-shorthand-properties@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz#6333aee2f8d6ee7e28615457298934a3b46198f0" - integrity sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-spread@^7.2.0": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz#3103a9abe22f742b6d406ecd3cd49b774919b406" - integrity sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-sticky-regex@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz#a1e454b5995560a9c1e0d537dfc15061fd2687e1" - integrity sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.0.0" - -"@babel/plugin-transform-template-literals@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.2.0.tgz#d87ed01b8eaac7a92473f608c97c089de2ba1e5b" - integrity sha512-FkPix00J9A/XWXv4VoKJBMeSkyY9x/TqIh76wzcdfl57RJJcf8CehQ08uwfhCDNtRQYtHQKBTwKZDEyjE13Lwg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-typeof-symbol@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz#117d2bcec2fbf64b4b59d1f9819894682d29f2b2" - integrity sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - -"@babel/plugin-transform-unicode-regex@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.2.0.tgz#4eb8db16f972f8abb5062c161b8b115546ade08b" - integrity sha512-m48Y0lMhrbXEJnVUaYly29jRXbQ3ksxPrS1Tg8t+MHqzXhtBYAvI51euOBaoAlZLPHsieY9XPVMf80a5x0cPcA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.0.0" - regexpu-core "^4.1.3" - -"@babel/polyfill@^7.2.5": - version "7.2.5" - resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.2.5.tgz#6c54b964f71ad27edddc567d065e57e87ed7fa7d" - integrity sha512-8Y/t3MWThtMLYr0YNC/Q76tqN1w30+b0uQMeFUYauG2UGTR19zyUtFrAzT23zNtBxPp+LbE5E/nwV/q/r3y6ug== - dependencies: - core-js "^2.5.7" - regenerator-runtime "^0.12.0" - -"@babel/preset-env@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.4.tgz#887cf38b6d23c82f19b5135298bdb160062e33e1" - integrity sha512-2mwqfYMK8weA0g0uBKOt4FE3iEodiHy9/CW0b+nWXcbL+pGzLx8ESYc+j9IIxr6LTDHWKgPm71i9smo02bw+gA== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-async-generator-functions" "^7.2.0" - "@babel/plugin-proposal-json-strings" "^7.2.0" - "@babel/plugin-proposal-object-rest-spread" "^7.3.4" - "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.2.0" - "@babel/plugin-syntax-async-generators" "^7.2.0" - "@babel/plugin-syntax-json-strings" "^7.2.0" - "@babel/plugin-syntax-object-rest-spread" "^7.2.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" - "@babel/plugin-transform-arrow-functions" "^7.2.0" - "@babel/plugin-transform-async-to-generator" "^7.3.4" - "@babel/plugin-transform-block-scoped-functions" "^7.2.0" - "@babel/plugin-transform-block-scoping" "^7.3.4" - "@babel/plugin-transform-classes" "^7.3.4" - "@babel/plugin-transform-computed-properties" "^7.2.0" - "@babel/plugin-transform-destructuring" "^7.2.0" - "@babel/plugin-transform-dotall-regex" "^7.2.0" - "@babel/plugin-transform-duplicate-keys" "^7.2.0" - "@babel/plugin-transform-exponentiation-operator" "^7.2.0" - "@babel/plugin-transform-for-of" "^7.2.0" - "@babel/plugin-transform-function-name" "^7.2.0" - "@babel/plugin-transform-literals" "^7.2.0" - "@babel/plugin-transform-modules-amd" "^7.2.0" - "@babel/plugin-transform-modules-commonjs" "^7.2.0" - "@babel/plugin-transform-modules-systemjs" "^7.3.4" - "@babel/plugin-transform-modules-umd" "^7.2.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.3.0" - "@babel/plugin-transform-new-target" "^7.0.0" - "@babel/plugin-transform-object-super" "^7.2.0" - "@babel/plugin-transform-parameters" "^7.2.0" - "@babel/plugin-transform-regenerator" "^7.3.4" - "@babel/plugin-transform-shorthand-properties" "^7.2.0" - "@babel/plugin-transform-spread" "^7.2.0" - "@babel/plugin-transform-sticky-regex" "^7.2.0" - "@babel/plugin-transform-template-literals" "^7.2.0" - "@babel/plugin-transform-typeof-symbol" "^7.2.0" - "@babel/plugin-transform-unicode-regex" "^7.2.0" - browserslist "^4.3.4" - invariant "^2.2.2" - js-levenshtein "^1.1.3" - semver "^5.3.0" - -"@babel/runtime@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" - integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g== - dependencies: - regenerator-runtime "^0.12.0" - -"@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" - integrity sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.2.2" - "@babel/types" "^7.2.2" - -"@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06" - integrity sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.3.4" - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/parser" "^7.3.4" - "@babel/types" "^7.3.4" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.11" - -"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.4.tgz#bf482eaeaffb367a28abbf9357a94963235d90ed" - integrity sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ== - dependencies: - esutils "^2.0.2" - lodash "^4.17.11" - to-fast-properties "^2.0.0" - -"@csstools/convert-colors@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" - integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== - -"@rails/webpacker@^4.0.2": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-4.0.2.tgz#2c2e96527500b060a84159098449ddb1615c65e8" - integrity sha512-TDj/+UHnWaEg8X21E3cGKvptm3BbW1aUtOAXtrYwpK9tkiWq+Dc40Gm2RIZW7rU3jxDDBZgPRiqvr5B0dorIVw== - dependencies: - "@babel/core" "^7.3.4" - "@babel/plugin-proposal-class-properties" "^7.3.4" - "@babel/plugin-proposal-object-rest-spread" "^7.3.4" - "@babel/plugin-syntax-dynamic-import" "^7.2.0" - "@babel/plugin-transform-destructuring" "^7.3.2" - "@babel/plugin-transform-regenerator" "^7.3.4" - "@babel/plugin-transform-runtime" "^7.3.4" - "@babel/polyfill" "^7.2.5" - "@babel/preset-env" "^7.3.4" - "@babel/runtime" "^7.3.4" - babel-loader "^8.0.5" - babel-plugin-dynamic-import-node "^2.2.0" - babel-plugin-macros "^2.5.0" - case-sensitive-paths-webpack-plugin "^2.2.0" - compression-webpack-plugin "^2.0.0" - css-loader "^2.1.0" - file-loader "^3.0.1" - flatted "^2.0.0" - glob "^7.1.3" - js-yaml "^3.12.2" - mini-css-extract-plugin "^0.5.0" - node-sass "^4.11.0" - optimize-css-assets-webpack-plugin "^5.0.1" - path-complete-extname "^1.0.0" - pnp-webpack-plugin "^1.3.1" - postcss-flexbugs-fixes "^4.1.0" - postcss-import "^12.0.1" - postcss-loader "^3.0.0" - postcss-preset-env "^6.6.0" - postcss-safe-parser "^4.0.1" - sass-loader "^7.1.0" - style-loader "^0.23.1" - terser-webpack-plugin "^1.2.3" - webpack "^4.29.6" - webpack-assets-manifest "^3.1.1" - webpack-cli "^3.2.3" - webpack-sources "^1.3.0" - -"@types/q@^1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.1.tgz#48fd98c1561fe718b61733daed46ff115b496e18" - integrity sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA== - -"@webassemblyjs/ast@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" - integrity sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ== - dependencies: - "@webassemblyjs/helper-module-context" "1.8.5" - "@webassemblyjs/helper-wasm-bytecode" "1.8.5" - "@webassemblyjs/wast-parser" "1.8.5" - -"@webassemblyjs/floating-point-hex-parser@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz#1ba926a2923613edce496fd5b02e8ce8a5f49721" - integrity sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ== - -"@webassemblyjs/helper-api-error@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz#c49dad22f645227c5edb610bdb9697f1aab721f7" - integrity sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA== - -"@webassemblyjs/helper-buffer@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz#fea93e429863dd5e4338555f42292385a653f204" - integrity sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q== - -"@webassemblyjs/helper-code-frame@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz#9a740ff48e3faa3022b1dff54423df9aa293c25e" - integrity sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ== - dependencies: - "@webassemblyjs/wast-printer" "1.8.5" - -"@webassemblyjs/helper-fsm@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz#ba0b7d3b3f7e4733da6059c9332275d860702452" - integrity sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow== - -"@webassemblyjs/helper-module-context@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz#def4b9927b0101dc8cbbd8d1edb5b7b9c82eb245" - integrity sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g== - dependencies: - "@webassemblyjs/ast" "1.8.5" - mamacro "^0.0.3" - -"@webassemblyjs/helper-wasm-bytecode@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz#537a750eddf5c1e932f3744206551c91c1b93e61" - integrity sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ== - -"@webassemblyjs/helper-wasm-section@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz#74ca6a6bcbe19e50a3b6b462847e69503e6bfcbf" - integrity sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA== - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-buffer" "1.8.5" - "@webassemblyjs/helper-wasm-bytecode" "1.8.5" - "@webassemblyjs/wasm-gen" "1.8.5" - -"@webassemblyjs/ieee754@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz#712329dbef240f36bf57bd2f7b8fb9bf4154421e" - integrity sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.8.5.tgz#044edeb34ea679f3e04cd4fd9824d5e35767ae10" - integrity sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.8.5.tgz#a8bf3b5d8ffe986c7c1e373ccbdc2a0915f0cedc" - integrity sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw== - -"@webassemblyjs/wasm-edit@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz#962da12aa5acc1c131c81c4232991c82ce56e01a" - integrity sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q== - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-buffer" "1.8.5" - "@webassemblyjs/helper-wasm-bytecode" "1.8.5" - "@webassemblyjs/helper-wasm-section" "1.8.5" - "@webassemblyjs/wasm-gen" "1.8.5" - "@webassemblyjs/wasm-opt" "1.8.5" - "@webassemblyjs/wasm-parser" "1.8.5" - "@webassemblyjs/wast-printer" "1.8.5" - -"@webassemblyjs/wasm-gen@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz#54840766c2c1002eb64ed1abe720aded714f98bc" - integrity sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg== - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-wasm-bytecode" "1.8.5" - "@webassemblyjs/ieee754" "1.8.5" - "@webassemblyjs/leb128" "1.8.5" - "@webassemblyjs/utf8" "1.8.5" - -"@webassemblyjs/wasm-opt@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz#b24d9f6ba50394af1349f510afa8ffcb8a63d264" - integrity sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q== - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-buffer" "1.8.5" - "@webassemblyjs/wasm-gen" "1.8.5" - "@webassemblyjs/wasm-parser" "1.8.5" - -"@webassemblyjs/wasm-parser@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz#21576f0ec88b91427357b8536383668ef7c66b8d" - integrity sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw== - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-api-error" "1.8.5" - "@webassemblyjs/helper-wasm-bytecode" "1.8.5" - "@webassemblyjs/ieee754" "1.8.5" - "@webassemblyjs/leb128" "1.8.5" - "@webassemblyjs/utf8" "1.8.5" - -"@webassemblyjs/wast-parser@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz#e10eecd542d0e7bd394f6827c49f3df6d4eefb8c" - integrity sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg== - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/floating-point-hex-parser" "1.8.5" - "@webassemblyjs/helper-api-error" "1.8.5" - "@webassemblyjs/helper-code-frame" "1.8.5" - "@webassemblyjs/helper-fsm" "1.8.5" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/wast-printer@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz#114bbc481fd10ca0e23b3560fa812748b0bae5bc" - integrity sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg== - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/wast-parser" "1.8.5" - "@xtuc/long" "4.2.2" - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -accepts@~1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" - integrity sha1-hiRnWMfdbSGmR0/whKR0DsBesh8= - dependencies: - mime-types "~2.1.16" - negotiator "0.6.1" - -acorn-dynamic-import@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" - integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== - -acorn@^6.0.5: - version "6.1.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" - integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== - -"activetext@file:../..": - version "0.0.0" - -ajv-errors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" - integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== - -ajv-keywords@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be" - integrity sha1-rCsnk5xUPpXSwG5/f1wnvkqlQ74= - -ajv@^4.9.1: - version "4.11.8" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" - integrity sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY= - dependencies: - co "^4.6.0" - json-stable-stringify "^1.0.1" - -ajv@^6.1.0: - version "6.1.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.1.1.tgz#978d597fbc2b7d0e5a5c3ddeb149a682f2abfa0e" - integrity sha1-l41Zf7wrfQ5aXD3esUmmgvKr+g4= - dependencies: - fast-deep-equal "^1.0.0" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.3.0" - -ajv@^6.5.5: - version "6.10.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" - integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== - dependencies: - fast-deep-equal "^2.0.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -alphanum-sort@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" - integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= - -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= - -ansi-colors@^3.0.0: - version "3.2.4" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" - integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== - -ansi-html@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" - integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" - -aproba@^1.0.3, aproba@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -are-we-there-yet@~1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" - integrity sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0= - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - -argparse@^1.0.7: - version "1.0.9" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" - integrity sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY= - dependencies: - sprintf-js "~1.0.2" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= - -array-flatten@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296" - integrity sha1-Qmu52oQJDBg42BLIFQryCoMx4pY= - -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= - dependencies: - array-uniq "^1.0.1" - -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -asn1.js@^4.0.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a" - integrity sha512-b/OsSjvWEo8Pi8H0zsDd2P6Uqo2TK2pH8gNLSJtNLM2Db0v2QaAZ0pBQJXVjAn4gBuugeVDr7s63ZogpUIwWDg== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -asn1@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" - integrity sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y= - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - -assert-plus@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" - integrity sha1-104bh+ev/A24qttwIfP+SBAasjQ= - -assert@^1.1.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" - integrity sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE= - dependencies: - util "0.10.3" - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -async-each@^1.0.0, async-each@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" - integrity sha1-GdOGodntxufByF04iu28xW0zYC0= - -async-foreach@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" - integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI= - -async@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -atob@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.0.3.tgz#19c7a760473774468f20b2d2d03372ad7d4cbf5d" - integrity sha1-GcenYEc3dEaPILLS0DNyrX1Mv10= - -autoprefixer@^9.4.9: - version "9.4.10" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.4.10.tgz#e1be61fc728bacac8f4252ed242711ec0dcc6a7b" - integrity sha512-XR8XZ09tUrrSzgSlys4+hy5r2/z4Jp7Ag3pHm31U4g/CTccYPOVe19AkaJ4ey/vRd1sfj+5TtuD6I0PXtutjvQ== - dependencies: - browserslist "^4.4.2" - caniuse-lite "^1.0.30000940" - normalize-range "^0.1.2" - num2fraction "^1.2.2" - postcss "^7.0.14" - postcss-value-parser "^3.3.1" - -aws-sign2@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" - integrity sha1-FDQt0428yU0OW4fXY81jYSwOeU8= - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.2.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" - integrity sha1-g+9cqGCysy5KDe7e6MdxudtXRx4= - -aws4@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" - integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== - -babel-loader@^8.0.5: - version "8.0.5" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.5.tgz#225322d7509c2157655840bba52e46b6c2f2fe33" - integrity sha512-NTnHnVRd2JnRqPC0vW+iOQWU5pchDbYXsG2E6DMXEpMfUcQKclF9gmf3G3ZMhzG7IG9ji4coL0cm+FxeWxDpnw== - dependencies: - find-cache-dir "^2.0.0" - loader-utils "^1.0.2" - mkdirp "^0.5.1" - util.promisify "^1.0.0" - -babel-plugin-dynamic-import-node@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.2.0.tgz#c0adfb07d95f4a4495e9aaac6ec386c4d7c2524e" - integrity sha512-fP899ELUnTaBcIzmrW7nniyqqdYWrWuJUyPWHxFa/c7r7hS6KC8FscNfLlBNIoPSc55kYMGEEKjPjJGCLbE1qA== - dependencies: - object.assign "^4.1.0" - -babel-plugin-macros@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.5.0.tgz#01f4d3b50ed567a67b80a30b9da066e94f4097b6" - integrity sha512-BWw0lD0kVZAXRD3Od1kMrdmfudqzDzYv2qrN3l2ISR1HVp1EgLKfbOrYV9xmY5k3qx3RIu5uPAUZZZHpo0o5Iw== - dependencies: - cosmiconfig "^5.0.5" - resolve "^1.8.1" - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -base64-js@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" - integrity sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw== - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -batch@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" - integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= - -bcrypt-pbkdf@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" - integrity sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40= - dependencies: - tweetnacl "^0.14.3" - -big.js@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" - integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^1.0.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" - integrity sha1-RqoXUftqL5PuXmibsQh9SxTGwgU= - -block-stream@*: - version "0.0.9" - resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" - integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= - dependencies: - inherits "~2.0.0" - -bluebird@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" - integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: - version "4.11.8" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" - integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== - -body-parser@1.18.2: - version "1.18.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" - integrity sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ= - dependencies: - bytes "3.0.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.1" - http-errors "~1.6.2" - iconv-lite "0.4.19" - on-finished "~2.3.0" - qs "6.5.1" - raw-body "2.3.2" - type-is "~1.6.15" - -bonjour@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" - integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= - dependencies: - array-flatten "^2.1.0" - deep-equal "^1.0.1" - dns-equal "^1.0.0" - dns-txt "^2.0.2" - multicast-dns "^6.0.1" - multicast-dns-service-types "^1.1.0" - -boolbase@^1.0.0, boolbase@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= - -boom@2.x.x: - version "2.10.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" - integrity sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8= - dependencies: - hoek "2.x.x" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.0.tgz#a46941cb5fb492156b3d6a656e06c35364e3e66e" - integrity sha512-P4O8UQRdGiMLWSizsApmXVQDBS6KCt7dSexgLKBmH5Hr1CZq7vsnscFh8oR1sP1ab1Zj0uCHCEzZeV6SfUf3rA== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - define-property "^1.0.0" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@^2.3.1, braces@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -brorand@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f" - integrity sha512-UGnTYAnB2a3YuYKIRy1/4FB2HdM866E0qC46JXvVTYKlBlZlnvfpSfY6OKfXZAkv70eJ2a1SqzpAo5CRhZGDFg== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" - integrity sha1-mYgkSHS/XtTijalWZtzWasj8Njo= - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" - integrity sha1-2qJ3cXRwki7S/hhZQRihdUOXId0= - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - -browserify-rsa@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" - integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= - dependencies: - bn.js "^4.1.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" - integrity sha1-qk62jl17ZYuqa/alfmMMvXqT0pg= - dependencies: - bn.js "^4.1.1" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.2" - elliptic "^6.0.0" - inherits "^2.0.1" - parse-asn1 "^5.0.0" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.4.2.tgz#6ea8a74d6464bb0bd549105f659b41197d8f0ba2" - integrity sha512-ISS/AIAiHERJ3d45Fz0AVYKkgcy+F/eJHzKEvv1j0wwKGKD9T3BrwKr/5g45L+Y4XIK5PlTqefHciRFcfE1Jxg== - dependencies: - caniuse-lite "^1.0.30000939" - electron-to-chromium "^1.3.113" - node-releases "^1.1.8" - -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -buffer-indexof@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" - integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - -buffer@^4.3.0: - version "4.9.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" - integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -builtin-modules@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= - -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= - -cacache@^11.0.2, cacache@^11.2.0: - version "11.3.2" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.2.tgz#2d81e308e3d258ca38125b676b98b2ac9ce69bfa" - integrity sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg== - dependencies: - bluebird "^3.5.3" - chownr "^1.1.1" - figgy-pudding "^3.5.1" - glob "^7.1.3" - graceful-fs "^4.1.15" - lru-cache "^5.1.1" - mississippi "^3.0.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - promise-inflight "^1.0.1" - rimraf "^2.6.2" - ssri "^6.0.1" - unique-filename "^1.1.1" - y18n "^4.0.0" - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" - integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= - dependencies: - callsites "^2.0.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" - integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= - dependencies: - caller-callsite "^2.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= - -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= - -camelcase@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" - integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= - -camelcase@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" - integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= - -camelcase@^5.0.0, camelcase@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.2.0.tgz#e7522abda5ed94cc0489e1b8466610e88404cf45" - integrity sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ== - -caniuse-api@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" - integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== - dependencies: - browserslist "^4.0.0" - caniuse-lite "^1.0.0" - lodash.memoize "^4.1.2" - lodash.uniq "^4.5.0" - -caniuse-lite@^1.0.0: - version "1.0.30000808" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000808.tgz#7d759b5518529ea08b6705a19e70dbf401628ffc" - integrity sha512-vT0JLmHdvq1UVbYXioxCXHYdNw55tyvi+IUWyX0Zeh1OFQi2IllYtm38IJnSgHWCv/zUnX1hdhy3vMJvuTNSqw== - -caniuse-lite@^1.0.30000939, caniuse-lite@^1.0.30000940: - version "1.0.30000942" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000942.tgz#454139b28274bce70bfe1d50c30970df7430c6e4" - integrity sha512-wLf+IhZUy2rfz48tc40OH7jHjXjnvDFEYqBHluINs/6MgzoNLPf25zhE4NOVzqxLKndf+hau81sAW0RcGHIaBQ== - -case-sensitive-paths-webpack-plugin@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.2.0.tgz#3371ef6365ef9c25fa4b81c16ace0e9c7dc58c3e" - integrity sha512-u5ElzokS8A1pm9vM3/iDgTcI3xqHxuCao94Oz8etI3cf0Tio0p8izkDYbTIn09uP3yUUr6+veaE6IkjnTYS46g== - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -chalk@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0, chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chokidar@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.1.tgz#6e67e9998fe10e8f651e975ca62460456ff8e297" - integrity sha512-rv5iP8ENhpqvDWr677rAXcB+SMoPQ1urd4ch79+PhM4lQwbATdJUQK69t0lJIKNB+VXpqxt5V1gvqs59XEPKnw== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.0" - braces "^2.3.0" - glob-parent "^3.1.0" - inherits "^2.0.1" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^2.1.1" - path-is-absolute "^1.0.0" - readdirp "^2.0.0" - upath "1.0.0" - optionalDependencies: - fsevents "^1.0.0" - -chokidar@^2.0.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.2.tgz#9c23ea40b01638439e0513864d362aeacc5ad058" - integrity sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.0" - optionalDependencies: - fsevents "^1.2.7" - -chownr@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" - integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== - -chrome-trace-event@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz#45a91bd2c20c9411f0963b5aaeb9a1b95e09cc48" - integrity sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A== - dependencies: - tslib "^1.9.0" - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -cliui@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" - integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.0.0" - -cliui@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" - integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== - dependencies: - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi "^2.0.0" - -clone-deep@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713" - integrity sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ== - dependencies: - for-own "^1.0.0" - is-plain-object "^2.0.4" - kind-of "^6.0.0" - shallow-clone "^1.0.0" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= - -coa@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" - integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== - dependencies: - "@types/q" "^1.5.1" - chalk "^2.4.1" - q "^1.1.2" - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0, color-convert@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" - integrity sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ== - dependencies: - color-name "^1.1.1" - -color-name@^1.0.0, color-name@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-string@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.2.tgz#26e45814bc3c9a7cbd6751648a41434514a773a9" - integrity sha1-JuRYFLw8mny9Z1FkikFDRRSnc6k= - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/color/-/color-3.1.0.tgz#d8e9fb096732875774c84bf922815df0308d0ffc" - integrity sha512-CwyopLkuRYO5ei2EpzpIh6LqJMt6Mt+jZhO5VI5f/wJLZriXQE32/SSqzmrh+QB+AZT81Cj8yv+7zwToW8ahZg== - dependencies: - color-convert "^1.9.1" - color-string "^1.5.2" - -combined-stream@^1.0.5, combined-stream@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" - integrity sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk= - dependencies: - delayed-stream "~1.0.0" - -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" - integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== - dependencies: - delayed-stream "~1.0.0" - -commander@~2.17.1: - version "2.17.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" - integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= - -component-emitter@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" - integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= - -compressible@~2.0.11: - version "2.0.12" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.12.tgz#c59a5c99db76767e9876500e271ef63b3493bd66" - integrity sha1-xZpcmdt2dn6YdlAOJx72OzSTvWY= - dependencies: - mime-db ">= 1.30.0 < 2" - -compression-webpack-plugin@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-2.0.0.tgz#46476350c1eb27f783dccc79ac2f709baa2cffbc" - integrity sha512-bDgd7oTUZC8EkRx8j0sjyCfeiO+e5sFcfgaFcjVhfQf5lLya7oY2BczxcJ7IUuVjz5m6fy8IECFmVFew3xLk8Q== - dependencies: - cacache "^11.2.0" - find-cache-dir "^2.0.0" - neo-async "^2.5.0" - schema-utils "^1.0.0" - serialize-javascript "^1.4.0" - webpack-sources "^1.0.1" - -compression@^1.5.2: - version "1.7.1" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.1.tgz#eff2603efc2e22cf86f35d2eb93589f9875373db" - integrity sha1-7/JgPvwuIs+G810uuTWJ+YdTc9s= - dependencies: - accepts "~1.3.4" - bytes "3.0.0" - compressible "~2.0.11" - debug "2.6.9" - on-headers "~1.0.1" - safe-buffer "5.1.1" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -concat-stream@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" - integrity sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc= - dependencies: - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -connect-history-api-fallback@^1.3.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a" - integrity sha1-sGhzk0vF40T+9hGhlqb6rgruAVo= - -console-browserify@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" - integrity sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA= - dependencies: - date-now "^0.1.4" - -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= - -content-disposition@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" - integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -convert-source-map@^1.1.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" - integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== - dependencies: - safe-buffer "~5.1.1" - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= - -cookie@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" - integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= - -copy-concurrently@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" - integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== - dependencies: - aproba "^1.1.1" - fs-write-stream-atomic "^1.0.8" - iferr "^0.1.5" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.0" - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -core-js@^2.5.7: - version "2.6.5" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895" - integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A== - -core-util-is@1.0.2, core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -cosmiconfig@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-4.0.0.tgz#760391549580bbd2df1e562bc177b13c290972dc" - integrity sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ== - dependencies: - is-directory "^0.3.1" - js-yaml "^3.9.0" - parse-json "^4.0.0" - require-from-string "^2.0.1" - -cosmiconfig@^5.0.0, cosmiconfig@^5.0.5: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.1.0.tgz#6c5c35e97f37f985061cdf653f114784231185cf" - integrity sha512-kCNPvthka8gvLtzAxQXvWo4FxqRB+ftRZyPZNuab5ngvM9Y7yw7hbEysglptLgpkGX9nAOKTBVkHUAe8xtYR6Q== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.9.0" - lodash.get "^4.4.2" - parse-json "^4.0.0" - -create-ecdh@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" - integrity sha1-iIxyNZbN92EvZJgjPuvXo1MBc30= - dependencies: - bn.js "^4.1.0" - elliptic "^6.0.0" - -create-hash@^1.1.0, create-hash@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" - integrity sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0= - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - ripemd160 "^2.0.0" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: - version "1.1.6" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" - integrity sha1-rLniIaThe9sHbpBlfEK5PjcmzwY= - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -cross-spawn@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" - integrity sha1-ElYDfsufDF9549bvE14wdwGEuYI= - dependencies: - lru-cache "^4.0.1" - which "^1.2.9" - -cross-spawn@^6.0.0, cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -cryptiles@2.x.x: - version "2.0.5" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" - integrity sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g= - dependencies: - boom "2.x.x" - -crypto-browserify@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -css-blank-pseudo@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5" - integrity sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w== - dependencies: - postcss "^7.0.5" - -css-color-names@0.0.4, css-color-names@^0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" - integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= - -css-declaration-sorter@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22" - integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA== - dependencies: - postcss "^7.0.1" - timsort "^0.3.0" - -css-has-pseudo@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz#3c642ab34ca242c59c41a125df9105841f6966ee" - integrity sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^5.0.0-rc.4" - -css-loader@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea" - integrity sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w== - dependencies: - camelcase "^5.2.0" - icss-utils "^4.1.0" - loader-utils "^1.2.3" - normalize-path "^3.0.0" - postcss "^7.0.14" - postcss-modules-extract-imports "^2.0.0" - postcss-modules-local-by-default "^2.0.6" - postcss-modules-scope "^2.1.0" - postcss-modules-values "^2.0.0" - postcss-value-parser "^3.3.0" - schema-utils "^1.0.0" - -css-prefers-color-scheme@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz#6f830a2714199d4f0d0d0bb8a27916ed65cff1f4" - integrity sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg== - dependencies: - postcss "^7.0.5" - -css-select-base-adapter@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" - integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== - -css-select@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.0.2.tgz#ab4386cec9e1f668855564b17c3733b43b2a5ede" - integrity sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ== - dependencies: - boolbase "^1.0.0" - css-what "^2.1.2" - domutils "^1.7.0" - nth-check "^1.0.2" - -css-tree@1.0.0-alpha.28: - version "1.0.0-alpha.28" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.28.tgz#8e8968190d886c9477bc8d61e96f61af3f7ffa7f" - integrity sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w== - dependencies: - mdn-data "~1.1.0" - source-map "^0.5.3" - -css-tree@1.0.0-alpha.29: - version "1.0.0-alpha.29" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39" - integrity sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg== - dependencies: - mdn-data "~1.1.0" - source-map "^0.5.3" - -css-unit-converter@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996" - integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY= - -css-url-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/css-url-regex/-/css-url-regex-1.1.0.tgz#83834230cc9f74c457de59eebd1543feeb83b7ec" - integrity sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w= - -css-what@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" - integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== - -cssdb@^4.3.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0" - integrity sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ== - -cssesc@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" - integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg== - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -cssnano-preset-default@^4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76" - integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA== - dependencies: - css-declaration-sorter "^4.0.1" - cssnano-util-raw-cache "^4.0.1" - postcss "^7.0.0" - postcss-calc "^7.0.1" - postcss-colormin "^4.0.3" - postcss-convert-values "^4.0.1" - postcss-discard-comments "^4.0.2" - postcss-discard-duplicates "^4.0.2" - postcss-discard-empty "^4.0.1" - postcss-discard-overridden "^4.0.1" - postcss-merge-longhand "^4.0.11" - postcss-merge-rules "^4.0.3" - postcss-minify-font-values "^4.0.2" - postcss-minify-gradients "^4.0.2" - postcss-minify-params "^4.0.2" - postcss-minify-selectors "^4.0.2" - postcss-normalize-charset "^4.0.1" - postcss-normalize-display-values "^4.0.2" - postcss-normalize-positions "^4.0.2" - postcss-normalize-repeat-style "^4.0.2" - postcss-normalize-string "^4.0.2" - postcss-normalize-timing-functions "^4.0.2" - postcss-normalize-unicode "^4.0.1" - postcss-normalize-url "^4.0.1" - postcss-normalize-whitespace "^4.0.2" - postcss-ordered-values "^4.1.2" - postcss-reduce-initial "^4.0.3" - postcss-reduce-transforms "^4.0.2" - postcss-svgo "^4.0.2" - postcss-unique-selectors "^4.0.1" - -cssnano-util-get-arguments@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f" - integrity sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8= - -cssnano-util-get-match@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d" - integrity sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0= - -cssnano-util-raw-cache@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282" - integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA== - dependencies: - postcss "^7.0.0" - -cssnano-util-same-parent@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" - integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== - -cssnano@^4.1.0: - version "4.1.10" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2" - integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ== - dependencies: - cosmiconfig "^5.0.0" - cssnano-preset-default "^4.0.7" - is-resolvable "^1.0.0" - postcss "^7.0.0" - -csso@^3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b" - integrity sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg== - dependencies: - css-tree "1.0.0-alpha.29" - -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" - -cyclist@~0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" - integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - -date-now@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" - integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= - -debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.2.5, debug@^3.2.6: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -debug@^4.1.0, debug@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== - dependencies: - ms "^2.1.1" - -decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decamelize@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-2.0.0.tgz#656d7bbc8094c4c788ea53c5840908c9c7d063c7" - integrity sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg== - dependencies: - xregexp "4.0.0" - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -deep-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" - integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -deep-extend@~0.4.0: - version "0.4.2" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" - integrity sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8= - -default-gateway@^4.0.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" - integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== - dependencies: - execa "^1.0.0" - ip-regex "^2.1.0" - -define-properties@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" - integrity sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ= - dependencies: - foreach "^2.0.5" - object-keys "^1.0.8" - -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -del@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" - integrity sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU= - dependencies: - globby "^6.1.0" - is-path-cwd "^1.0.0" - is-path-in-cwd "^1.0.0" - p-map "^1.1.1" - pify "^3.0.0" - rimraf "^2.2.8" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - -depd@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" - integrity sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k= - -depd@~1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - -des.js@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" - integrity sha1-wHTS4qpqipoH29YfmhXCzYPsjsw= - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= - -detect-file@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" - integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= - -detect-libc@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - -detect-node@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" - integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== - -diffie-hellman@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" - integrity sha1-tYNXOScM/ias9jIJn97SoH8gnl4= - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -dns-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" - integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= - -dns-packet@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" - integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== - dependencies: - ip "^1.1.0" - safe-buffer "^5.0.1" - -dns-txt@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" - integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= - dependencies: - buffer-indexof "^1.0.0" - -dom-serializer@0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" - integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== - dependencies: - domelementtype "^1.3.0" - entities "^1.1.1" - -domain-browser@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - -domelementtype@1, domelementtype@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" - integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== - -domutils@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - -dot-prop@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" - integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ== - dependencies: - is-obj "^1.0.0" - -duplexify@^3.4.2, duplexify@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.3.tgz#8b5818800df92fd0125b27ab896491912858243e" - integrity sha512-g8ID9OroF9hKt2POf8YLayy+9594PzmM3scI00/uBXocX3TWNgoB67hjzkFe9ITAbQOne/lLdBxHXvYUM4ZgGA== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - -ecc-jsbn@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" - integrity sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU= - dependencies: - jsbn "~0.1.0" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - -electron-to-chromium@^1.3.113: - version "1.3.113" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz#b1ccf619df7295aea17bc6951dc689632629e4a9" - integrity sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g== - -elliptic@^6.0.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" - integrity sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8= - dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" - hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" - -emojis-list@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" - integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= - -encodeurl@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= - -end-of-stream@^1.0.0, end-of-stream@^1.1.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" - integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== - dependencies: - once "^1.4.0" - -enhanced-resolve@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" - integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.4.0" - tapable "^1.0.0" - -entities@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" - integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== - -errno@^0.1.3, errno@^0.1.4: - version "0.1.6" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.6.tgz#c386ce8a6283f14fc09563b71560908c9bf53026" - integrity sha512-IsORQDpaaSwcDP4ZZnHxgE85werpo34VYn1Ud3mq+eUsF593faR8oCZNXrROVkpFu2TsbrNhHin0aUrTsQ9vNw== - dependencies: - prr "~1.0.1" - -error-ex@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" - integrity sha1-+FWobOYa3E6GIcPNoh56dhLDqNw= - dependencies: - is-arrayish "^0.2.1" - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.12.0, es-abstract@^1.5.1: - version "1.13.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" - integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== - dependencies: - es-to-primitive "^1.2.0" - function-bind "^1.1.1" - has "^1.0.3" - is-callable "^1.1.4" - is-regex "^1.0.4" - object-keys "^1.0.12" - -es-to-primitive@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" - integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -eslint-scope@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.2.tgz#5f10cd6cabb1965bf479fa65745673439e21cb0e" - integrity sha512-5q1+B/ogmHl8+paxtOKx38Z8LtWkVGuNt3+GQNErqwLl6ViNp/gdJGMCjZNxZ8j/VYjDNZ2Fo+eQc1TAVPIzbg== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -esprima@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" - integrity sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw== - -esrecurse@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" - integrity sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM= - dependencies: - estraverse "^4.1.0" - object-assign "^4.0.1" - -estraverse@^4.1.0, estraverse@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" - integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= - -esutils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" - integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= - -eventemitter3@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" - integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== - -events@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" - integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= - -eventsource@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" - integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== - dependencies: - original "^1.0.0" - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expand-tilde@^2.0.0, expand-tilde@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" - integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= - dependencies: - homedir-polyfill "^1.0.1" - -express@^4.16.2: - version "4.16.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" - integrity sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w= - dependencies: - accepts "~1.3.4" - array-flatten "1.1.1" - body-parser "1.18.2" - content-disposition "0.5.2" - content-type "~1.0.4" - cookie "0.3.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.1" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.1.0" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.2" - path-to-regexp "0.1.7" - proxy-addr "~2.0.2" - qs "6.5.1" - range-parser "~1.2.0" - safe-buffer "5.1.1" - send "0.16.1" - serve-static "1.13.1" - setprototypeof "1.1.0" - statuses "~1.3.1" - type-is "~1.6.15" - utils-merge "1.0.1" - vary "~1.1.2" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@~3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" - integrity sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ= - -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -extglob@^2.0.2, extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - -fast-deep-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" - integrity sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8= - -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - -fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= - -faye-websocket@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" - integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ= - dependencies: - websocket-driver ">=0.5.1" - -faye-websocket@~0.11.1: - version "0.11.1" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38" - integrity sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg= - dependencies: - websocket-driver ">=0.5.1" - -figgy-pudding@^3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" - integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== - -file-loader@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa" - integrity sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw== - dependencies: - loader-utils "^1.0.2" - schema-utils "^1.0.0" - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -finalhandler@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" - integrity sha1-zgtoVbRYU+eRsvzGgARtiCU91/U= - dependencies: - debug "2.6.9" - encodeurl "~1.0.1" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.2" - statuses "~1.3.1" - unpipe "~1.0.0" - -find-cache-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d" - integrity sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA== - dependencies: - commondir "^1.0.1" - make-dir "^1.0.0" - pkg-dir "^3.0.0" - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -findup-sync@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" - integrity sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw= - dependencies: - detect-file "^1.0.0" - is-glob "^3.1.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - -flatted@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916" - integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg== - -flatten@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" - integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I= - -flush-write-stream@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417" - integrity sha1-yBuQ2HRnZvGmCaRoCZRsRd2K5Bc= - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.4" - -follow-redirects@^1.0.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" - integrity sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ== - dependencies: - debug "^3.2.6" - -for-in@^0.1.3: - version "0.1.8" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" - integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE= - -for-in@^1.0.1, for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -for-own@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" - integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= - dependencies: - for-in "^1.0.1" - -foreach@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -form-data@~2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" - integrity sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE= - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.12" - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= - -from2@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" - integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.0" - -fs-minipass@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" - integrity sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ== - dependencies: - minipass "^2.2.1" - -fs-write-stream-atomic@^1.0.8: - version "1.0.10" - resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" - integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= - dependencies: - graceful-fs "^4.1.2" - iferr "^0.1.5" - imurmurhash "^0.1.4" - readable-stream "1 || 2" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8" - integrity sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q== - dependencies: - nan "^2.3.0" - node-pre-gyp "^0.6.39" - -fsevents@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.7.tgz#4851b664a3783e52003b3c66eb0eee1074933aa4" - integrity sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw== - dependencies: - nan "^2.9.2" - node-pre-gyp "^0.10.0" - -fstream-ignore@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" - integrity sha1-nDHa40dnAY/h0kmyTa2mfQktoQU= - dependencies: - fstream "^1.0.0" - inherits "2" - minimatch "^3.0.0" - -fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: - version "1.0.11" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" - integrity sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE= - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - -function-bind@^1.0.2, function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -gaze@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105" - integrity sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU= - dependencies: - globule "^1.0.0" - -get-caller-file@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" - integrity sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U= - -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= - -get-stream@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" - integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI= - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@~7.1.1: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.1.3: - version "7.1.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" - integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-modules@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" - integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== - dependencies: - global-prefix "^1.0.1" - is-windows "^1.0.1" - resolve-dir "^1.0.0" - -global-prefix@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" - integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= - dependencies: - expand-tilde "^2.0.2" - homedir-polyfill "^1.0.1" - ini "^1.3.4" - is-windows "^1.0.1" - which "^1.2.14" - -globals@^11.1.0: - version "11.11.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.11.0.tgz#dcf93757fa2de5486fbeed7118538adf789e9c2e" - integrity sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw== - -globby@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" - integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= - dependencies: - array-union "^1.0.1" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -globule@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09" - integrity sha1-HcScaCLdnoovoAuiopUAboZkvQk= - dependencies: - glob "~7.1.1" - lodash "~4.17.4" - minimatch "~3.0.2" - -graceful-fs@^4.1.11, graceful-fs@^4.1.2: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" - integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= - -graceful-fs@^4.1.15: - version "4.1.15" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" - integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== - -handle-thing@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" - integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== - -har-schema@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" - integrity sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4= - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" - integrity sha1-M0gdDxu/9gDdID11gSpqX7oALio= - dependencies: - ajv "^4.9.1" - har-schema "^1.0.5" - -har-validator@~5.1.0: - version "5.1.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" - integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== - dependencies: - ajv "^6.5.5" - har-schema "^2.0.0" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" - integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= - -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has@^1.0.0, has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -has@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" - integrity sha1-hGFzP1OLCDfJNh45qauelwTcLyg= - dependencies: - function-bind "^1.0.2" - -hash-base@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" - integrity sha1-ZuodhW206KVHDK32/OI65SRO8uE= - dependencies: - inherits "^2.0.1" - -hash-base@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg= - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" - integrity sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.0" - -hawk@3.1.3, hawk@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" - integrity sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ= - dependencies: - boom "2.x.x" - cryptiles "2.x.x" - hoek "2.x.x" - sntp "1.x.x" - -hex-color-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" - integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== - -hmac-drbg@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -hoek@2.x.x: - version "2.16.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" - integrity sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0= - -homedir-polyfill@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" - integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== - dependencies: - parse-passwd "^1.0.0" - -hosted-git-info@^2.1.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" - integrity sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg== - -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" - integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -hsl-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" - integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4= - -hsla-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" - integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= - -html-comment-regex@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" - integrity sha1-ZouTd26q5V696POtRkswekljYl4= - -html-entities@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" - integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" - integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= - -http-errors@1.6.2, http-errors@~1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" - integrity sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY= - dependencies: - depd "1.1.1" - inherits "2.0.3" - setprototypeof "1.0.3" - statuses ">= 1.3.1 < 2" - -http-parser-js@>=0.4.0: - version "0.4.10" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" - integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= - -http-proxy-middleware@^0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" - integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== - dependencies: - http-proxy "^1.17.0" - is-glob "^4.0.0" - lodash "^4.17.11" - micromatch "^3.1.10" - -http-proxy@^1.17.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" - integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g== - dependencies: - eventemitter3 "^3.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -http-signature@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" - integrity sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8= - dependencies: - assert-plus "^0.2.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= - -iconv-lite@0.4.19: - version "0.4.19" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" - integrity sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ== - -iconv-lite@^0.4.4: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -icss-replace-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" - integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= - -icss-utils@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.0.tgz#339dbbffb9f8729a243b701e1c29d4cc58c52f0e" - integrity sha512-3DEun4VOeMvSczifM3F2cKQrDQ5Pj6WKhkOq6HD4QTnDUAq8MQRxy5TX6Sy1iY6WPBe4gQ3p5vTECjbIkglkkQ== - dependencies: - postcss "^7.0.14" - -ieee754@^1.1.4: - version "1.1.8" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" - integrity sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q= - -iferr@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" - integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= - -ignore-walk@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" - integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== - dependencies: - minimatch "^3.0.4" - -import-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" - integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk= - dependencies: - import-from "^2.1.0" - -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" - integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= - dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" - -import-from@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" - integrity sha1-M1238qev/VOqpHHUuAId7ja387E= - dependencies: - resolve-from "^3.0.0" - -import-local@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" - integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== - dependencies: - pkg-dir "^3.0.0" - resolve-cwd "^2.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -in-publish@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" - integrity sha1-4g/146KvwmkDILbcVSaCqcf631E= - -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= - dependencies: - repeating "^2.0.0" - -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= - -indexof@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" - integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= - -ini@^1.3.4, ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== - -internal-ip@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.2.0.tgz#46e81b638d84c338e5c67e42b1a17db67d0814fa" - integrity sha512-ZY8Rk+hlvFeuMmG5uH1MXhhdeMntmIaxaInvAmzMq/SHV8rv4Kh+6GiQNNDQd0wZFrcO+FiTBo8lui/osKOyJw== - dependencies: - default-gateway "^4.0.1" - ipaddr.js "^1.9.0" - -interpret@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" - integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== - -invariant@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" - integrity sha1-nh9WrArNtr8wMwbzOL47IErmA2A= - dependencies: - loose-envify "^1.0.0" - -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= - -invert-kv@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" - integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== - -ip-regex@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" - integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= - -ip@^1.1.0, ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= - -ipaddr.js@1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0" - integrity sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A= - -ipaddr.js@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" - integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== - -is-absolute-url@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" - integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-arrayish@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.1.tgz#c2dfc386abaa0c3e33c48db3fe87059e69065efd" - integrity sha1-wt/DhquqDD4zxI2z/ocFnmkGXv0= - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= - dependencies: - binary-extensions "^1.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-builtin-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" - integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74= - dependencies: - builtin-modules "^1.0.0" - -is-callable@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" - integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== - -is-color-stop@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" - integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U= - dependencies: - css-color-names "^0.0.4" - hex-color-regex "^1.1.0" - hsl-regex "^1.0.0" - hsla-regex "^1.0.0" - rgb-regex "^1.0.1" - rgba-regex "^1.0.0" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-date-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" - integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.0, is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-finite@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" - integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" - integrity sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A= - dependencies: - is-extglob "^2.1.1" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-obj@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" - integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= - -is-odd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-1.0.0.tgz#3b8a932eb028b3775c39bb09e91767accdb69088" - integrity sha1-O4qTLrAos3dcObsJ6RdnrM22kIg= - dependencies: - is-number "^3.0.0" - -is-path-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" - integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0= - -is-path-in-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" - integrity sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw= - dependencies: - is-path-inside "^1.0.0" - -is-path-inside@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" - integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= - dependencies: - path-is-inside "^1.0.1" - -is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-regex@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" - integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= - dependencies: - has "^1.0.1" - -is-resolvable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" - integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - -is-svg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" - integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ== - dependencies: - html-comment-regex "^1.1.0" - -is-symbol@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" - integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== - dependencies: - has-symbols "^1.0.0" - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - -is-windows@^1.0.1, is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= - -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - -js-base64@^2.1.8: - version "2.4.3" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582" - integrity sha512-H7ErYLM34CvDMto3GbD6xD0JLUGYXR3QTcH6B/tr4Hi/QpSThnCsIp+Sy5FRTw3B0d6py4HcNkW7nO/wdtGWEw== - -js-levenshtein@^1.1.3: - version "1.1.6" - resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" - integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g== - -js-tokens@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.12.0, js-yaml@^3.12.2, js-yaml@^3.9.0: - version "3.12.2" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.2.tgz#ef1d067c5a9d9cb65bd72f285b5d8105c77f14fc" - integrity sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= - -json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-schema-traverse@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" - integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= - -json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8= - dependencies: - jsonify "~0.0.0" - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - -json3@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" - integrity sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE= - -json5@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= - -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - -json5@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" - integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ== - dependencies: - minimist "^1.2.0" - -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" - integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= - -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -killable@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b" - integrity sha1-2ouEvUfeU5WHj5XWTQLyRJ/gXms= - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0, kind-of@^5.0.2: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" - integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== - -last-call-webpack-plugin@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555" - integrity sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w== - dependencies: - lodash "^4.17.5" - webpack-sources "^1.1.0" - -lazy-cache@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264" - integrity sha1-uRkKT5EzVGlIQIWfio9whNiCImQ= - dependencies: - set-getter "^0.1.0" - -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= - dependencies: - invert-kv "^1.0.0" - -lcid@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" - integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== - dependencies: - invert-kv "^2.0.0" - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -loader-runner@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" - integrity sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI= - -loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" - integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0= - dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - -loader-utils@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" - integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== - dependencies: - big.js "^5.2.2" - emojis-list "^2.0.0" - json5 "^1.0.1" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -lodash._reinterpolate@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" - integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= - -lodash.assign@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" - integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= - -lodash.clonedeep@^4.3.2: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - -lodash.get@^4.0, lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= - -lodash.has@^4.0: - version "4.5.2" - resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" - integrity sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI= - -lodash.memoize@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" - integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= - -lodash.mergewith@^4.6.0: - version "4.6.1" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" - integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ== - -lodash.tail@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" - integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ= - -lodash.template@^4.2.4: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" - integrity sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A= - dependencies: - lodash._reinterpolate "~3.0.0" - lodash.templatesettings "^4.0.0" - -lodash.templatesettings@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316" - integrity sha1-K01OlbpEDZFf8IvImeRVNmZxMxY= - dependencies: - lodash._reinterpolate "~3.0.0" - -lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= - -lodash@3.x: - version "3.10.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" - integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y= - -lodash@^4.0.0, lodash@~4.17.4: - version "4.17.5" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" - integrity sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw== - -lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== - -loglevel@^1.4.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" - integrity sha1-4PyVEztu8nbNyIh82vJKpvFW+Po= - -loose-envify@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" - integrity sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg= - dependencies: - js-tokens "^3.0.0" - -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - -lru-cache@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" - integrity sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -make-dir@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51" - integrity sha512-0Pkui4wLJ7rxvmfUvs87skoEaxmu0hCUApF8nonzpl7q//FWp9zu8W61Scz4sd/kUiqDxvUhtoam2efDyiBzcA== - dependencies: - pify "^3.0.0" - -mamacro@^0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4" - integrity sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA== - -map-age-cleaner@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-obj@^1.0.0, map-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -md5.js@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" - integrity sha1-6b296UogpawYsENA/Fdk1bCdkB0= - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -mdn-data@~1.1.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01" - integrity sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA== - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - -mem@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-4.1.0.tgz#aeb9be2d21f47e78af29e4ac5978e8afa2ca5b8a" - integrity sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg== - dependencies: - map-age-cleaner "^0.1.1" - mimic-fn "^1.0.0" - p-is-promise "^2.0.0" - -memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" - integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -meow@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= - -micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.8: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -micromatch@^3.1.4: - version "3.1.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.5.tgz#d05e168c206472dfbca985bfef4f57797b4cd4ba" - integrity sha512-ykttrLPQrz1PUJcXjwsTUjGoPJ64StIGNE2lGVD1c9CuguJ+L7/navsE8IcDNndOoCMvYV0qc/exfVbMHkUhvA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.0" - define-property "^1.0.0" - extend-shallow "^2.0.1" - extglob "^2.0.2" - fragment-cache "^0.2.1" - kind-of "^6.0.0" - nanomatch "^1.2.5" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -"mime-db@>= 1.30.0 < 2": - version "1.32.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.32.0.tgz#485b3848b01a3cda5f968b4882c0771e58e09414" - integrity sha512-+ZWo/xZN40Tt6S+HyakUxnSOgff+JEdaneLWIm0Z6LmpCn5DMcZntLyUY5c/rTDog28LhXLKOUZKoTxTCAdBVw== - -mime-db@~1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" - integrity sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE= - -mime-db@~1.38.0: - version "1.38.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" - integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg== - -mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.7: - version "2.1.17" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" - integrity sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo= - dependencies: - mime-db "~1.30.0" - -mime-types@~2.1.19: - version "2.1.22" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd" - integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog== - dependencies: - mime-db "~1.38.0" - -mime@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" - integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== - -mime@^2.3.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.0.tgz#e051fd881358585f3279df333fe694da0bcffdd6" - integrity sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w== - -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== - -mini-css-extract-plugin@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0" - integrity sha512-IuaLjruM0vMKhUUT51fQdQzBYTX49dLj8w68ALEAe2A4iYNpIC4eMac67mt3NzycvjOlf07/kYxJDc0RTl1Wqw== - dependencies: - loader-utils "^1.1.0" - schema-utils "^1.0.0" - webpack-sources "^1.1.0" - -minimalistic-assert@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" - integrity sha1-cCvi3aazf0g2vLP121ZkG2Sh09M= - -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - -"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - -minimist@^1.1.3, minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= - -minipass@^2.2.1, minipass@^2.3.4: - version "2.3.5" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" - integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== - dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" - -minizlib@^1.1.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" - integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== - dependencies: - minipass "^2.2.1" - -mississippi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" - integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== - dependencies: - concat-stream "^1.5.0" - duplexify "^3.4.2" - end-of-stream "^1.1.0" - flush-write-stream "^1.0.0" - from2 "^2.1.0" - parallel-transform "^1.1.0" - pump "^3.0.0" - pumpify "^1.3.3" - stream-each "^1.1.0" - through2 "^2.0.0" - -mixin-deep@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" - integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mixin-object@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" - integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4= - dependencies: - for-in "^0.1.3" - is-extendable "^0.1.1" - -mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - -move-concurrently@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" - integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I= - dependencies: - aproba "^1.1.1" - copy-concurrently "^1.0.0" - fs-write-stream-atomic "^1.0.8" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.3" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -multicast-dns-service-types@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" - integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= - -multicast-dns@^6.0.1: - version "6.2.3" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" - integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== - dependencies: - dns-packet "^1.3.1" - thunky "^1.0.2" - -nan@^2.10.0, nan@^2.9.2: - version "2.12.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" - integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw== - -nan@^2.3.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" - integrity sha1-7XFfP+neArV6XmJS2QqWZ14fCFo= - -nanomatch@^1.2.5: - version "1.2.7" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79" - integrity sha512-/5ldsnyurvEw7wNpxLFgjVvBLMta43niEYOy0CJ4ntcYSbx6bugRUTQeFb4BR/WanEL1o3aQgHuVLHQaB6tOqg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^1.0.0" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - is-odd "^1.0.0" - kind-of "^5.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -needle@^2.2.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e" - integrity sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA== - dependencies: - debug "^2.1.2" - iconv-lite "^0.4.4" - sax "^1.2.4" - -negotiator@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" - integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk= - -neo-async@^2.5.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.0.tgz#b9d15e4d71c6762908654b5183ed38b753340835" - integrity sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA== - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -node-forge@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300" - integrity sha1-naYR6giYL0uUIGs760zJZl8gwwA= - -node-gyp@^3.8.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" - integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== - dependencies: - fstream "^1.0.0" - glob "^7.0.3" - graceful-fs "^4.1.2" - mkdirp "^0.5.0" - nopt "2 || 3" - npmlog "0 || 1 || 2 || 3 || 4" - osenv "0" - request "^2.87.0" - rimraf "2" - semver "~5.3.0" - tar "^2.0.0" - which "1" - -node-libs-browser@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df" - integrity sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg== - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^1.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.0" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.10.3" - vm-browserify "0.0.4" - -node-pre-gyp@^0.10.0: - version "0.10.3" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" - integrity sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4" - -node-pre-gyp@^0.6.39: - version "0.6.39" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" - integrity sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ== - dependencies: - detect-libc "^1.0.2" - hawk "3.1.3" - mkdirp "^0.5.1" - nopt "^4.0.1" - npmlog "^4.0.2" - rc "^1.1.7" - request "2.81.0" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^2.2.1" - tar-pack "^3.4.0" - -node-releases@^1.1.8: - version "1.1.9" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.9.tgz#70d0985ec4bf7de9f08fc481f5dae111889ca482" - integrity sha512-oic3GT4OtbWWKfRolz5Syw0Xus0KRFxeorLNj0s93ofX6PWyuzKjsiGxsCtWktBwwmTF6DdRRf2KreGqeOk5KA== - dependencies: - semver "^5.3.0" - -node-sass@^4.11.0: - version "4.11.0" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a" - integrity sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA== - dependencies: - async-foreach "^0.1.3" - chalk "^1.1.1" - cross-spawn "^3.0.0" - gaze "^1.0.0" - get-stdin "^4.0.1" - glob "^7.0.3" - in-publish "^2.0.0" - lodash.assign "^4.2.0" - lodash.clonedeep "^4.3.2" - lodash.mergewith "^4.6.0" - meow "^3.7.0" - mkdirp "^0.5.1" - nan "^2.10.0" - node-gyp "^3.8.0" - npmlog "^4.0.0" - request "^2.88.0" - sass-graph "^2.2.4" - stdout-stream "^1.4.0" - "true-case-path" "^1.0.2" - -"nopt@2 || 3": - version "3.0.6" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= - dependencies: - abbrev "1" - -nopt@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" - integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= - dependencies: - abbrev "1" - osenv "^0.1.4" - -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: - version "2.4.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" - integrity sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw== - dependencies: - hosted-git-info "^2.1.4" - is-builtin-module "^1.0.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= - -normalize-url@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" - integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== - -npm-bundled@^1.0.1: - version "1.0.6" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" - integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== - -npm-packlist@^1.1.6: - version "1.4.1" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc" - integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw== - dependencies: - ignore-walk "^3.0.1" - npm-bundled "^1.0.1" - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -nth-check@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" - integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== - dependencies: - boolbase "~1.0.0" - -num2fraction@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" - integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -oauth-sign@~0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM= - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@^4.0.1, object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-keys@^1.0.11, object-keys@^1.0.12: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.0.tgz#11bd22348dd2e096a045ab06f6c85bcc340fa032" - integrity sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg== - -object-keys@^1.0.8: - version "1.0.11" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" - integrity sha1-xUYBd4rVYPEULODgG8yotW0TQm0= - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.assign@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== - dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" - -object.getownpropertydescriptors@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" - integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= - dependencies: - define-properties "^1.1.2" - es-abstract "^1.5.1" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -object.values@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9" - integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.12.0" - function-bind "^1.1.1" - has "^1.0.3" - -obuf@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.1.tgz#104124b6c602c6796881a042541d36db43a5264e" - integrity sha1-EEEktsYCxnlogaBCVB0220OlJk4= - -obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" - integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c= - -once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -opn@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/opn/-/opn-5.2.0.tgz#71fdf934d6827d676cecbea1531f95d354641225" - integrity sha512-Jd/GpzPyHF4P2/aNOVmS3lfMSWV9J7cOhCG1s08XCEAsPkB7lp6ddiU0J7XzyQRDUh8BqJ7PchfINjR8jyofRQ== - dependencies: - is-wsl "^1.1.0" - -optimize-css-assets-webpack-plugin@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.1.tgz#9eb500711d35165b45e7fd60ba2df40cb3eb9159" - integrity sha512-Rqm6sSjWtx9FchdP0uzTQDc7GXDKnwVEGoSxjezPkzMewx7gEWE9IMUYKmigTRC4U3RaNSwYVnUDLuIdtTpm0A== - dependencies: - cssnano "^4.1.0" - last-call-webpack-plugin "^3.0.0" - -original@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" - integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== - dependencies: - url-parse "^1.4.3" - -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= - dependencies: - lcid "^1.0.0" - -os-locale@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" - integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== - dependencies: - execa "^1.0.0" - lcid "^2.0.0" - mem "^4.0.0" - -os-tmpdir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -osenv@0, osenv@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" - integrity sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ= - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -p-is-promise@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.0.0.tgz#7554e3d572109a87e1f3f53f6a7d85d1b194f4c5" - integrity sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg== - -p-limit@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" - integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== - dependencies: - p-try "^2.0.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-map@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" - integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== - -p-try@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" - integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== - -pako@~1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" - integrity sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg== - -parallel-transform@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" - integrity sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY= - dependencies: - cyclist "~0.2.2" - inherits "^2.0.3" - readable-stream "^2.1.5" - -parse-asn1@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" - integrity sha1-N8T5t+06tlx0gXtfJICTf7+XxxI= - dependencies: - asn1.js "^4.0.0" - browserify-aes "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= - -parseurl@~1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" - integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M= - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" - integrity sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo= - -path-complete-extname@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/path-complete-extname/-/path-complete-extname-1.0.0.tgz#f889985dc91000c815515c0bfed06c5acda0752b" - integrity sha512-CVjiWcMRdGU8ubs08YQVzhutOR5DEfO97ipRIlOGMK5Bek5nQySknBpuxVAVJ36hseTNs+vdIcv57ZrWxH7zvg== - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-is-inside@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= - -path-key@^2.0.0, path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-parse@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" - integrity sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME= - -path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -pbkdf2@^3.0.3: - version "3.0.14" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade" - integrity sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU= - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -pify@^2.0.0, pify@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.0.0" - -pnp-webpack-plugin@^1.3.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.4.1.tgz#e8f8c683b496a71c0d200e664c4bb399a9c9585e" - integrity sha512-S4kz+5rvWvD0w1O63eTJeXIxW4JHK0wPRMO7GmPhbZXJnTePcfrWZlni4BoglIf7pLSY18xtqo3MSnVkoAFXKg== - dependencies: - ts-pnp "^1.0.0" - -portfinder@^1.0.9: - version "1.0.13" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9" - integrity sha1-uzLs2HwnEErm7kS1o8y/Drsa7ek= - dependencies: - async "^1.5.2" - debug "^2.2.0" - mkdirp "0.5.x" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -postcss-attribute-case-insensitive@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.1.tgz#b2a721a0d279c2f9103a36331c88981526428cc7" - integrity sha512-L2YKB3vF4PetdTIthQVeT+7YiSzMoNMLLYxPXXppOOP7NoazEAy45sh2LvJ8leCQjfBcfkYQs8TtCcQjeZTp8A== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0" - -postcss-calc@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.1.tgz#36d77bab023b0ecbb9789d84dcb23c4941145436" - integrity sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ== - dependencies: - css-unit-converter "^1.1.1" - postcss "^7.0.5" - postcss-selector-parser "^5.0.0-rc.4" - postcss-value-parser "^3.3.1" - -postcss-color-functional-notation@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz#5efd37a88fbabeb00a2966d1e53d98ced93f74e0" - integrity sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-color-gray@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz#532a31eb909f8da898ceffe296fdc1f864be8547" - integrity sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw== - dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.5" - postcss-values-parser "^2.0.0" - -postcss-color-hex-alpha@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.2.tgz#e9b1886bb038daed33f6394168c210b40bb4fdb6" - integrity sha512-8bIOzQMGdZVifoBQUJdw+yIY00omBd2EwkJXepQo9cjp1UOHHHoeRDeSzTP6vakEpaRc6GAIOfvcQR7jBYaG5Q== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-color-mod-function@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz#816ba145ac11cc3cb6baa905a75a49f903e4d31d" - integrity sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ== - dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-color-rebeccapurple@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz#c7a89be872bb74e45b1e3022bfe5748823e6de77" - integrity sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-colormin@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381" - integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw== - dependencies: - browserslist "^4.0.0" - color "^3.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-convert-values@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f" - integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ== - dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-custom-media@^7.0.7: - version "7.0.7" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.7.tgz#bbc698ed3089ded61aad0f5bfb1fb48bf6969e73" - integrity sha512-bWPCdZKdH60wKOTG4HKEgxWnZVjAIVNOJDvi3lkuTa90xo/K0YHa2ZnlKLC5e2qF8qCcMQXt0yzQITBp8d0OFA== - dependencies: - postcss "^7.0.5" - -postcss-custom-properties@^8.0.9: - version "8.0.9" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-8.0.9.tgz#8943870528a6eae4c8e8d285b6ccc9fd1f97e69c" - integrity sha512-/Lbn5GP2JkKhgUO2elMs4NnbUJcvHX4AaF5nuJDaNkd2chYW1KA5qtOGGgdkBEWcXtKSQfHXzT7C6grEVyb13w== - dependencies: - postcss "^7.0.5" - postcss-values-parser "^2.0.0" - -postcss-custom-selectors@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz#64858c6eb2ecff2fb41d0b28c9dd7b3db4de7fba" - integrity sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" - -postcss-dir-pseudo-class@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz#6e3a4177d0edb3abcc85fdb6fbb1c26dabaeaba2" - integrity sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" - -postcss-discard-comments@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" - integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg== - dependencies: - postcss "^7.0.0" - -postcss-discard-duplicates@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb" - integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ== - dependencies: - postcss "^7.0.0" - -postcss-discard-empty@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765" - integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w== - dependencies: - postcss "^7.0.0" - -postcss-discard-overridden@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57" - integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg== - dependencies: - postcss "^7.0.0" - -postcss-double-position-gradients@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz#fc927d52fddc896cb3a2812ebc5df147e110522e" - integrity sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA== - dependencies: - postcss "^7.0.5" - postcss-values-parser "^2.0.0" - -postcss-env-function@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-2.0.2.tgz#0f3e3d3c57f094a92c2baf4b6241f0b0da5365d7" - integrity sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-flexbugs-fixes@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.1.0.tgz#e094a9df1783e2200b7b19f875dcad3b3aff8b20" - integrity sha512-jr1LHxQvStNNAHlgco6PzY308zvLklh7SJVYuWUwyUQncofaAlD2l+P/gxKHOdqWKe7xJSkVLFF/2Tp+JqMSZA== - dependencies: - postcss "^7.0.0" - -postcss-focus-visible@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz#477d107113ade6024b14128317ade2bd1e17046e" - integrity sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g== - dependencies: - postcss "^7.0.2" - -postcss-focus-within@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz#763b8788596cee9b874c999201cdde80659ef680" - integrity sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w== - dependencies: - postcss "^7.0.2" - -postcss-font-variant@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-4.0.0.tgz#71dd3c6c10a0d846c5eda07803439617bbbabacc" - integrity sha512-M8BFYKOvCrI2aITzDad7kWuXXTm0YhGdP9Q8HanmN4EF1Hmcgs1KK5rSHylt/lUJe8yLxiSwWAHdScoEiIxztg== - dependencies: - postcss "^7.0.2" - -postcss-gap-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz#431c192ab3ed96a3c3d09f2ff615960f902c1715" - integrity sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg== - dependencies: - postcss "^7.0.2" - -postcss-image-set-function@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz#28920a2f29945bed4c3198d7df6496d410d3f288" - integrity sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-import@^12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153" - integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw== - dependencies: - postcss "^7.0.1" - postcss-value-parser "^3.2.3" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-initial@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.0.tgz#1772512faf11421b791fb2ca6879df5f68aa0517" - integrity sha512-WzrqZ5nG9R9fUtrA+we92R4jhVvEB32IIRTzfIG/PLL8UV4CvbF1ugTEHEFX6vWxl41Xt5RTCJPEZkuWzrOM+Q== - dependencies: - lodash.template "^4.2.4" - postcss "^7.0.2" - -postcss-lab-function@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz#bb51a6856cd12289ab4ae20db1e3821ef13d7d2e" - integrity sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg== - dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-load-config@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.0.0.tgz#f1312ddbf5912cd747177083c5ef7a19d62ee484" - integrity sha512-V5JBLzw406BB8UIfsAWSK2KSwIJ5yoEIVFb4gVkXci0QdKgA24jLmHZ/ghe/GgX0lJ0/D1uUK1ejhzEY94MChQ== - dependencies: - cosmiconfig "^4.0.0" - import-cwd "^2.0.0" - -postcss-loader@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d" - integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA== - dependencies: - loader-utils "^1.1.0" - postcss "^7.0.0" - postcss-load-config "^2.0.0" - schema-utils "^1.0.0" - -postcss-logical@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-3.0.0.tgz#2495d0f8b82e9f262725f75f9401b34e7b45d5b5" - integrity sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA== - dependencies: - postcss "^7.0.2" - -postcss-media-minmax@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5" - integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw== - dependencies: - postcss "^7.0.2" - -postcss-merge-longhand@^4.0.11: - version "4.0.11" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24" - integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw== - dependencies: - css-color-names "0.0.4" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - stylehacks "^4.0.0" - -postcss-merge-rules@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650" - integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ== - dependencies: - browserslist "^4.0.0" - caniuse-api "^3.0.0" - cssnano-util-same-parent "^4.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" - vendors "^1.0.0" - -postcss-minify-font-values@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6" - integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg== - dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-minify-gradients@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471" - integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q== - dependencies: - cssnano-util-get-arguments "^4.0.0" - is-color-stop "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-minify-params@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874" - integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg== - dependencies: - alphanum-sort "^1.0.0" - browserslist "^4.0.0" - cssnano-util-get-arguments "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - uniqs "^2.0.0" - -postcss-minify-selectors@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8" - integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g== - dependencies: - alphanum-sort "^1.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" - -postcss-modules-extract-imports@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" - integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== - dependencies: - postcss "^7.0.5" - -postcss-modules-local-by-default@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz#dd9953f6dd476b5fd1ef2d8830c8929760b56e63" - integrity sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^6.0.0" - postcss-value-parser "^3.3.1" - -postcss-modules-scope@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz#ad3f5bf7856114f6fcab901b0502e2a2bc39d4eb" - integrity sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^6.0.0" - -postcss-modules-values@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz#479b46dc0c5ca3dc7fa5270851836b9ec7152f64" - integrity sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w== - dependencies: - icss-replace-symbols "^1.1.0" - postcss "^7.0.6" - -postcss-nesting@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-7.0.0.tgz#6e26a770a0c8fcba33782a6b6f350845e1a448f6" - integrity sha512-WSsbVd5Ampi3Y0nk/SKr5+K34n52PqMqEfswu6RtU4r7wA8vSD+gM8/D9qq4aJkHImwn1+9iEFTbjoWsQeqtaQ== - dependencies: - postcss "^7.0.2" - -postcss-normalize-charset@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4" - integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g== - dependencies: - postcss "^7.0.0" - -postcss-normalize-display-values@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a" - integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ== - dependencies: - cssnano-util-get-match "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-positions@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f" - integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA== - dependencies: - cssnano-util-get-arguments "^4.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-repeat-style@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c" - integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q== - dependencies: - cssnano-util-get-arguments "^4.0.0" - cssnano-util-get-match "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-string@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c" - integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA== - dependencies: - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-timing-functions@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9" - integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A== - dependencies: - cssnano-util-get-match "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-unicode@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb" - integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg== - dependencies: - browserslist "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-url@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1" - integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA== - dependencies: - is-absolute-url "^2.0.0" - normalize-url "^3.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-whitespace@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82" - integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA== - dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-ordered-values@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee" - integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw== - dependencies: - cssnano-util-get-arguments "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-overflow-shorthand@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz#31ecf350e9c6f6ddc250a78f0c3e111f32dd4c30" - integrity sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g== - dependencies: - postcss "^7.0.2" - -postcss-page-break@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-2.0.0.tgz#add52d0e0a528cabe6afee8b46e2abb277df46bf" - integrity sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ== - dependencies: - postcss "^7.0.2" - -postcss-place@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-4.0.1.tgz#e9f39d33d2dc584e46ee1db45adb77ca9d1dcc62" - integrity sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-preset-env@^6.6.0: - version "6.6.0" - resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-6.6.0.tgz#642e7d962e2bdc2e355db117c1eb63952690ed5b" - integrity sha512-I3zAiycfqXpPIFD6HXhLfWXIewAWO8emOKz+QSsxaUZb9Dp8HbF5kUf+4Wy/AxR33o+LRoO8blEWCHth0ZsCLA== - dependencies: - autoprefixer "^9.4.9" - browserslist "^4.4.2" - caniuse-lite "^1.0.30000939" - css-blank-pseudo "^0.1.4" - css-has-pseudo "^0.10.0" - css-prefers-color-scheme "^3.1.1" - cssdb "^4.3.0" - postcss "^7.0.14" - postcss-attribute-case-insensitive "^4.0.1" - postcss-color-functional-notation "^2.0.1" - postcss-color-gray "^5.0.0" - postcss-color-hex-alpha "^5.0.2" - postcss-color-mod-function "^3.0.3" - postcss-color-rebeccapurple "^4.0.1" - postcss-custom-media "^7.0.7" - postcss-custom-properties "^8.0.9" - postcss-custom-selectors "^5.1.2" - postcss-dir-pseudo-class "^5.0.0" - postcss-double-position-gradients "^1.0.0" - postcss-env-function "^2.0.2" - postcss-focus-visible "^4.0.0" - postcss-focus-within "^3.0.0" - postcss-font-variant "^4.0.0" - postcss-gap-properties "^2.0.0" - postcss-image-set-function "^3.0.1" - postcss-initial "^3.0.0" - postcss-lab-function "^2.0.1" - postcss-logical "^3.0.0" - postcss-media-minmax "^4.0.0" - postcss-nesting "^7.0.0" - postcss-overflow-shorthand "^2.0.0" - postcss-page-break "^2.0.0" - postcss-place "^4.0.1" - postcss-pseudo-class-any-link "^6.0.0" - postcss-replace-overflow-wrap "^3.0.0" - postcss-selector-matches "^4.0.0" - postcss-selector-not "^4.0.0" - -postcss-pseudo-class-any-link@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz#2ed3eed393b3702879dec4a87032b210daeb04d1" - integrity sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" - -postcss-reduce-initial@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df" - integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA== - dependencies: - browserslist "^4.0.0" - caniuse-api "^3.0.0" - has "^1.0.0" - postcss "^7.0.0" - -postcss-reduce-transforms@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29" - integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg== - dependencies: - cssnano-util-get-match "^4.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-replace-overflow-wrap@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz#61b360ffdaedca84c7c918d2b0f0d0ea559ab01c" - integrity sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw== - dependencies: - postcss "^7.0.2" - -postcss-safe-parser@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.1.tgz#8756d9e4c36fdce2c72b091bbc8ca176ab1fcdea" - integrity sha512-xZsFA3uX8MO3yAda03QrG3/Eg1LN3EPfjjf07vke/46HERLZyHrTsQ9E1r1w1W//fWEhtYNndo2hQplN2cVpCQ== - dependencies: - postcss "^7.0.0" - -postcss-selector-matches@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz#71c8248f917ba2cc93037c9637ee09c64436fcff" - integrity sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww== - dependencies: - balanced-match "^1.0.0" - postcss "^7.0.2" - -postcss-selector-not@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-4.0.0.tgz#c68ff7ba96527499e832724a2674d65603b645c0" - integrity sha512-W+bkBZRhqJaYN8XAnbbZPLWMvZD1wKTu0UxtFKdhtGjWYmxhkUneoeOhRJKdAE5V7ZTlnbHfCR+6bNwK9e1dTQ== - dependencies: - balanced-match "^1.0.0" - postcss "^7.0.2" - -postcss-selector-parser@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865" - integrity sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU= - dependencies: - dot-prop "^4.1.1" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss-selector-parser@^5.0.0, postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" - integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== - dependencies: - cssesc "^2.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss-selector-parser@^6.0.0: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" - integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== - dependencies: - cssesc "^3.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss-svgo@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258" - integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw== - dependencies: - is-svg "^3.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - svgo "^1.0.0" - -postcss-unique-selectors@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac" - integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg== - dependencies: - alphanum-sort "^1.0.0" - postcss "^7.0.0" - uniqs "^2.0.0" - -postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" - integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== - -postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" - integrity sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU= - -postcss-values-parser@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz#da8b472d901da1e205b47bdc98637b9e9e550e5f" - integrity sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg== - dependencies: - flatten "^1.0.2" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.14" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.14.tgz#4527ed6b1ca0d82c53ce5ec1a2041c2346bbd6e5" - integrity sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -private@^0.1.6: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== - -process-nextick-args@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" - integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -promise-inflight@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" - integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= - -proxy-addr@~2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec" - integrity sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew= - dependencies: - forwarded "~0.1.2" - ipaddr.js "1.5.2" - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - -psl@^1.1.24: - version "1.1.31" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184" - integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw== - -public-encrypt@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" - integrity sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY= - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - -pump@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pumpify@^1.3.3: - version "1.4.0" - resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb" - integrity sha512-2kmNR9ry+Pf45opRVirpNuIFotsxUGLaYqxIwuR77AYrYRMuFCz9eryHBS52L360O+NcR383CL4QYlMKPq4zYA== - dependencies: - duplexify "^3.5.3" - inherits "^2.0.3" - pump "^2.0.0" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.2.4, punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -q@^1.1.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= - -qs@6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" - integrity sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A== - -qs@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" - integrity sha1-E+JtKK1rD/qpExLNO/cI7TUecjM= - -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - -querystring-es3@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -querystringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef" - integrity sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg== - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" - integrity sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A== - dependencies: - safe-buffer "^5.1.0" - -randomfill@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.3.tgz#b96b7df587f01dd91726c418f30553b1418e3d62" - integrity sha512-YL6GrhrWoic0Eq8rXVbMptH7dAxCs0J+mh5Y0euNekPPYaxEmdVGim6GdoxoRzKW2yJoU8tueifS7mYxvcFDEQ== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -range-parser@^1.0.3, range-parser@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" - integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= - -raw-body@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" - integrity sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k= - dependencies: - bytes "3.0.0" - http-errors "1.6.2" - iconv-lite "0.4.19" - unpipe "1.0.0" - -rc@^1.1.7: - version "1.2.5" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd" - integrity sha1-J1zWh/bjs2zHVrqibf7oCnkDAf0= - dependencies: - deep-extend "~0.4.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -rc@^1.2.7: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -read-cache@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" - integrity sha1-5mTvMRYRZsl1HNvo28+GtftY93Q= - dependencies: - pify "^2.3.0" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3: - version "2.3.4" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071" - integrity sha512-vuYxeWYM+fde14+rajzqgeohAI7YoJcHE7kXDAc4Nk0EbuKnJfqtY9YtRkLo/tqkuF7MsBQRhPnPeyjYITp3ZQ== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.0.3" - util-deprecate "~1.0.1" - -readable-stream@^3.0.6: - version "3.2.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d" - integrity sha512-RV20kLjdmpZuTF1INEb9IA3L68Nmi+Ri7ppZqo78wj//Pn62fCoJyV9zalccNzDD/OuJpMG4f+pfMl8+L6QdGw== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" - integrity sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg= - dependencies: - graceful-fs "^4.1.2" - minimatch "^3.0.2" - readable-stream "^2.0.2" - set-immediate-shim "^1.0.1" - -readdirp@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - -regenerate-unicode-properties@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.1.tgz#58a4a74e736380a7ab3c5f7e03f303a941b31289" - integrity sha512-HTjMafphaH5d5QDHuwW8Me6Hbc/GhXg8luNqTkPVwZ/oCZhnoifjWhGYsu2BzepMELTlbnoVcXvV0f+2uDDvoQ== - dependencies: - regenerate "^1.4.0" - -regenerate@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" - integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== - -regenerator-runtime@^0.12.0: - version "0.12.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" - integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== - -regenerator-transform@^0.13.4: - version "0.13.4" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb" - integrity sha512-T0QMBjK3J0MtxjPmdIMXm72Wvj2Abb0Bd4HADdfijwMdoIsyQZ6fWC7kDFhk2YinBBEMZDL7Y7wh0J1sGx3S4A== - dependencies: - private "^0.1.6" - -regex-not@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.0.tgz#42f83e39771622df826b02af176525d6a5f157f9" - integrity sha1-Qvg+OXcWIt+CawKvF2Ul1qXxV/k= - dependencies: - extend-shallow "^2.0.1" - -regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -regexp-tree@^0.1.0: - version "0.1.5" - resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.5.tgz#7cd71fca17198d04b4176efd79713f2998009397" - integrity sha512-nUmxvfJyAODw+0B13hj8CFVAxhe7fDEAgJgaotBu3nnR+IgGgZq59YedJP5VYTlkEfqjuK6TuRpnymKdatLZfQ== - -regexpu-core@^4.1.3, regexpu-core@^4.2.0: - version "4.5.3" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.3.tgz#72f572e03bb8b9f4f4d895a0ccc57e707f4af2e4" - integrity sha512-LON8666bTAlViVEPXMv65ZqiaR3rMNLz36PIaQ7D+er5snu93k0peR7FSvO0QteYbZ3GOkvfHKbGr/B1xDu9FA== - dependencies: - regenerate "^1.4.0" - regenerate-unicode-properties "^8.0.1" - regjsgen "^0.5.0" - regjsparser "^0.6.0" - unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.1.0" - -regjsgen@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd" - integrity sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA== - -regjsparser@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.0.tgz#f1e6ae8b7da2bae96c99399b868cd6c933a2ba9c" - integrity sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ== - dependencies: - jsesc "~0.5.0" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -repeat-element@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" - integrity sha1-7wiaF40Ug7quTZPrmLT55OEdmQo= - -repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - -request@2.81.0: - version "2.81.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" - integrity sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA= - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~4.2.1" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - performance-now "^0.2.0" - qs "~6.4.0" - safe-buffer "^5.0.1" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "^0.6.0" - uuid "^3.0.0" - -request@^2.87.0, request@^2.88.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-from-string@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= - -resolve-cwd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" - integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= - dependencies: - resolve-from "^3.0.0" - -resolve-dir@^1.0.0, resolve-dir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" - integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= - dependencies: - expand-tilde "^2.0.0" - global-modules "^1.0.0" - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@^1.1.7: - version "1.5.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" - integrity sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw== - dependencies: - path-parse "^1.0.5" - -resolve@^1.3.2, resolve@^1.8.1: - version "1.10.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" - integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== - dependencies: - path-parse "^1.0.6" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -rgb-regex@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" - integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= - -rgba-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" - integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= - -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1: - version "2.6.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" - integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w== - dependencies: - glob "^7.0.5" - -rimraf@^2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" - integrity sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc= - dependencies: - hash-base "^2.0.0" - inherits "^2.0.1" - -run-queue@^1.0.0, run-queue@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" - integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec= - dependencies: - aproba "^1.1.1" - -safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" - integrity sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg== - -safe-buffer@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sass-graph@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" - integrity sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k= - dependencies: - glob "^7.0.0" - lodash "^4.0.0" - scss-tokenizer "^0.2.3" - yargs "^7.0.0" - -sass-loader@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.1.0.tgz#16fd5138cb8b424bf8a759528a1972d72aad069d" - integrity sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w== - dependencies: - clone-deep "^2.0.1" - loader-utils "^1.0.1" - lodash.tail "^4.1.1" - neo-async "^2.5.0" - pify "^3.0.0" - semver "^5.5.0" - -sax@^1.2.4, sax@~1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -schema-utils@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" - integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== - dependencies: - ajv "^6.1.0" - ajv-errors "^1.0.0" - ajv-keywords "^3.1.0" - -scss-tokenizer@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" - integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE= - dependencies: - js-base64 "^2.1.8" - source-map "^0.4.2" - -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" - integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= - -selfsigned@^1.9.1: - version "1.10.2" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.2.tgz#b4449580d99929b65b10a48389301a6592088758" - integrity sha1-tESVgNmZKbZbEKSDiTAaZZIIh1g= - dependencies: - node-forge "0.7.1" - -"semver@2 || 3 || 4 || 5", semver@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" - integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== - -semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" - integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== - -semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" - integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= - -send@0.16.1: - version "0.16.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3" - integrity sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A== - dependencies: - debug "2.6.9" - depd "~1.1.1" - destroy "~1.0.4" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.6.2" - mime "1.4.1" - ms "2.0.0" - on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.3.1" - -serialize-javascript@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.4.0.tgz#7c958514db6ac2443a8abc062dc9f7886a7f6005" - integrity sha1-fJWFFNtqwkQ6irwGLcn3iGp/YAU= - -serve-index@^1.7.2: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" - integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= - dependencies: - accepts "~1.3.4" - batch "0.6.1" - debug "2.6.9" - escape-html "~1.0.3" - http-errors "~1.6.2" - mime-types "~2.1.17" - parseurl "~1.3.2" - -serve-static@1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719" - integrity sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ== - dependencies: - encodeurl "~1.0.1" - escape-html "~1.0.3" - parseurl "~1.3.2" - send "0.16.1" - -set-blocking@^2.0.0, set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-getter@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376" - integrity sha1-12nBgsnVpR9AkUXy+6guXoboA3Y= - dependencies: - to-object-path "^0.3.0" - -set-immediate-shim@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" - integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= - -set-value@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" - integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE= - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.1" - to-object-path "^0.3.0" - -set-value@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" - integrity sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - -setprototypeof@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" - integrity sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ= - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.10" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.10.tgz#b1fde5cd7d11a5626638a07c604ab909cfa31f9b" - integrity sha512-vnwmrFDlOExK4Nm16J2KMWHLrp14lBrjxMxBJpu++EnsuBmpiYaM/MEs46Vxxm/4FvdP5yTwuCTO9it5FSjrqA== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -shallow-clone@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571" - integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA== - dependencies: - is-extendable "^0.1.1" - kind-of "^5.0.0" - mixin-object "^2.0.1" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -signal-exit@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= - -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= - dependencies: - is-arrayish "^0.3.1" - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.1.tgz#e12b5487faded3e3dea0ac91e9400bf75b401370" - integrity sha1-4StUh/re0+PeoKyR6UAL91tAE3A= - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^2.0.0" - -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" - integrity sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg= - dependencies: - hoek "2.x.x" - -sockjs-client@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177" - integrity sha512-R9jxEzhnnrdxLCNln0xg5uGHqMnkhPSTzUZH2eXcR03S/On9Yvoq2wyUZILRUhZCNVu2PmwWVoyuiPz8th8zbg== - dependencies: - debug "^3.2.5" - eventsource "^1.0.7" - faye-websocket "~0.11.1" - inherits "^2.0.3" - json3 "^3.3.2" - url-parse "^1.4.3" - -sockjs@0.3.19: - version "0.3.19" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d" - integrity sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw== - dependencies: - faye-websocket "^0.10.0" - uuid "^3.0.1" - -source-list-map@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" - integrity sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A== - -source-map-resolve@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.1.tgz#7ad0f593f2281598e854df80f19aae4b92d7a11a" - integrity sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A== - dependencies: - atob "^2.0.0" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-support@~0.5.9: - version "0.5.10" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c" - integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" - integrity sha1-66T12pwNyZneaAMti092FzZSA2s= - dependencies: - amdefine ">=0.0.4" - -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -spdx-correct@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" - integrity sha1-SzBz2TP/UfORLwOsVRlJikFQ20A= - dependencies: - spdx-license-ids "^1.0.2" - -spdx-expression-parse@~1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" - integrity sha1-m98vIOH0DtRH++JzJmGR/O1RYmw= - -spdx-license-ids@^1.0.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" - integrity sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc= - -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - dependencies: - debug "^4.1.0" - detect-node "^2.0.4" - hpack.js "^2.1.6" - obuf "^1.1.2" - readable-stream "^3.0.6" - wbuf "^1.7.3" - -spdy@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.0.tgz#81f222b5a743a329aa12cea6a390e60e9b613c52" - integrity sha512-ot0oEGT/PGUpzf/6uk4AWLqkq+irlqHXkrdbk51oWONh3bxQmBuljxPNl66zlRRcIJStWq0QkLUCPOPjgjvU0Q== - dependencies: - debug "^4.1.0" - handle-thing "^2.0.0" - http-deceiver "^1.2.7" - select-hose "^2.0.0" - spdy-transport "^3.0.0" - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -sshpk@^1.7.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" - integrity sha1-US322mKHFEMW3EwY/hzx2UBzm+M= - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - dashdash "^1.12.0" - getpass "^0.1.1" - optionalDependencies: - bcrypt-pbkdf "^1.0.0" - ecc-jsbn "~0.1.1" - jsbn "~0.1.0" - tweetnacl "~0.14.0" - -ssri@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" - integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== - dependencies: - figgy-pudding "^3.5.1" - -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -"statuses@>= 1.3.1 < 2": - version "1.4.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" - integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== - -statuses@~1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" - integrity sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4= - -stdout-stream@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b" - integrity sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s= - dependencies: - readable-stream "^2.0.1" - -stream-browserify@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" - integrity sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds= - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - -stream-each@^1.1.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd" - integrity sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA== - dependencies: - end-of-stream "^1.1.0" - stream-shift "^1.0.0" - -stream-http@^2.7.2: - version "2.8.0" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10" - integrity sha512-sZOFxI/5xw058XIRHl4dU3dZ+TTOIGJR78Dvo0oEAejIt4ou27k+3ne1zYmCV+v7UucbxIFQuOgnkTVHh8YPnw== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.3.3" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - -stream-shift@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" - integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= - -string-width@^1.0.1, string-width@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -string-width@^2.0.0, string-width@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string_decoder@^1.0.0, string_decoder@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" - integrity sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ== - dependencies: - safe-buffer "~5.1.0" - -string_decoder@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" - integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w== - dependencies: - safe-buffer "~5.1.0" - -stringstream@~0.0.4: - version "0.0.5" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" - integrity sha1-TkhM1N5aC7vuGORjB3EKioFiGHg= - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= - dependencies: - get-stdin "^4.0.1" - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -style-loader@^0.23.1: - version "0.23.1" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.1.tgz#cb9154606f3e771ab6c4ab637026a1049174d925" - integrity sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg== - dependencies: - loader-utils "^1.1.0" - schema-utils "^1.0.0" - -stylehacks@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" - integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g== - dependencies: - browserslist "^4.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^5.3.0, supports-color@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - -svgo@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.2.0.tgz#305a8fc0f4f9710828c65039bb93d5793225ffc3" - integrity sha512-xBfxJxfk4UeVN8asec9jNxHiv3UAMv/ujwBWGYvQhhMb2u3YTGKkiybPcLFDLq7GLLWE9wa73e0/m8L5nTzQbw== - dependencies: - chalk "^2.4.1" - coa "^2.0.2" - css-select "^2.0.0" - css-select-base-adapter "^0.1.1" - css-tree "1.0.0-alpha.28" - css-url-regex "^1.1.0" - csso "^3.5.1" - js-yaml "^3.12.0" - mkdirp "~0.5.1" - object.values "^1.1.0" - sax "~1.2.4" - stable "^0.1.8" - unquote "~1.1.1" - util.promisify "~1.0.0" - -tapable@^1.0.0, tapable@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.1.tgz#4d297923c5a72a42360de2ab52dadfaaec00018e" - integrity sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA== - -tar-pack@^3.4.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" - integrity sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg== - dependencies: - debug "^2.2.0" - fstream "^1.0.10" - fstream-ignore "^1.0.5" - once "^1.3.3" - readable-stream "^2.1.4" - rimraf "^2.5.1" - tar "^2.2.1" - uid-number "^0.0.6" - -tar@^2.0.0, tar@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" - integrity sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE= - dependencies: - block-stream "*" - fstream "^1.0.2" - inherits "2" - -tar@^4: - version "4.4.8" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d" - integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ== - dependencies: - chownr "^1.1.1" - fs-minipass "^1.2.5" - minipass "^2.3.4" - minizlib "^1.1.1" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.2" - -terser-webpack-plugin@^1.1.0, terser-webpack-plugin@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.3.tgz#3f98bc902fac3e5d0de730869f50668561262ec8" - integrity sha512-GOK7q85oAb/5kE12fMuLdn2btOS9OBZn4VsecpHDywoUC/jLhSAKOiYo0ezx7ss2EXPMzyEWFoE0s1WLE+4+oA== - dependencies: - cacache "^11.0.2" - find-cache-dir "^2.0.0" - schema-utils "^1.0.0" - serialize-javascript "^1.4.0" - source-map "^0.6.1" - terser "^3.16.1" - webpack-sources "^1.1.0" - worker-farm "^1.5.2" - -terser@^3.16.1: - version "3.16.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-3.16.1.tgz#5b0dd4fa1ffd0b0b43c2493b2c364fd179160493" - integrity sha512-JDJjgleBROeek2iBcSNzOHLKsB/MdDf+E/BOAJ0Tk9r7p9/fVobfv7LMJ/g/k3v9SXdmjZnIlFd5nfn/Rt0Xow== - dependencies: - commander "~2.17.1" - source-map "~0.6.1" - source-map-support "~0.5.9" - -through2@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" - integrity sha1-AARWmzfHx0ujnEPzzteNGtlBQL4= - dependencies: - readable-stream "^2.1.5" - xtend "~4.0.1" - -thunky@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.2.tgz#a862e018e3fb1ea2ec3fce5d55605cf57f247371" - integrity sha1-qGLgGOP7HqLsP85dVWBc9X8kc3E= - -timers-browserify@^2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.6.tgz#241e76927d9ca05f4d959819022f5b3664b64bae" - integrity sha512-HQ3nbYRAowdVd0ckGFvmJPPCOH/CHleFN/Y0YQCX1DVaB7t+KFvisuyN09fuP8Jtp1CpfSh8O8bMkHbdbPe6Pw== - dependencies: - setimmediate "^1.0.4" - -timsort@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" - integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= - -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.1.tgz#15358bee4a2c83bd76377ba1dc049d0f18837aae" - integrity sha1-FTWL7kosg712N3uh3ASdDxiDeq4= - dependencies: - define-property "^0.2.5" - extend-shallow "^2.0.1" - regex-not "^1.0.0" - -to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -tough-cookie@~2.3.0: - version "2.3.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" - integrity sha1-C2GKVWW23qkL80JdBNVe3EdadWE= - dependencies: - punycode "^1.4.1" - -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= - -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= - -"true-case-path@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62" - integrity sha1-fskRMJJHZsf1c74wIMNPj9/QDWI= - dependencies: - glob "^6.0.4" - -ts-pnp@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.0.1.tgz#fde74a6371676a167abaeda1ffc0fdb423520098" - integrity sha512-Zzg9XH0anaqhNSlDRibNC8Kp+B9KNM0uRIpLpGkGyrgRIttA7zZBhotTSEoEyuDrz3QW2LGtu2dxuk34HzIGnQ== - -tslib@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" - integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== - -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - -type-is@~1.6.15: - version "1.6.15" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" - integrity sha1-yrEPtJCeRByChC6v4a1kbIGARBA= - dependencies: - media-typer "0.3.0" - mime-types "~2.1.15" - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - -uid-number@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" - integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE= - -underscore.string@2.3.x: - version "2.3.3" - resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-2.3.3.tgz#71c08bf6b428b1133f37e78fa3a21c82f7329b0d" - integrity sha1-ccCL9rQosRM/N+ePo6Icgvcymw0= - -unicode-canonical-property-names-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" - integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== - -unicode-match-property-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" - integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== - dependencies: - unicode-canonical-property-names-ecmascript "^1.0.4" - unicode-property-aliases-ecmascript "^1.0.4" - -unicode-match-property-value-ecmascript@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277" - integrity sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g== - -unicode-property-aliases-ecmascript@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57" - integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw== - -union-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" - integrity sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ= - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^0.4.3" - -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= - -uniqs@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" - integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= - -unique-filename@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" - integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== - dependencies: - unique-slug "^2.0.0" - -unique-slug@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab" - integrity sha1-22Z258fMBimHj/GWCXx4hVrp9Ks= - dependencies: - imurmurhash "^0.1.4" - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -unquote@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" - integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -upath@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.0.tgz#b4706b9461ca8473adf89133d235689ca17f3656" - integrity sha1-tHBrlGHKhHOt+JEz0jVonKF/NlY= - dependencies: - lodash "3.x" - underscore.string "2.3.x" - -upath@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.1.tgz#497f7c1090b0818f310bbfb06783586a68d28014" - integrity sha512-D0yetkpIOKiZQquxjM2Syvy48Y1DbZ0SWxgsZiwd9GCWRpc75vN8ytzem14WDSg+oiX6+Qt31FpiS/ExODCrLg== - -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== - dependencies: - punycode "^2.1.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -url-parse@^1.4.3: - version "1.4.4" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.4.tgz#cac1556e95faa0303691fec5cf9d5a1bc34648f8" - integrity sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg== - dependencies: - querystringify "^2.0.0" - requires-port "^1.0.0" - -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -use@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/use/-/use-2.0.2.tgz#ae28a0d72f93bf22422a18a2e379993112dec8e8" - integrity sha1-riig1y+TvyJCKhii43mZMRLeyOg= - dependencies: - define-property "^0.2.5" - isobject "^3.0.0" - lazy-cache "^2.0.2" - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util.promisify@^1.0.0, util.promisify@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" - integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== - dependencies: - define-properties "^1.1.2" - object.getownpropertydescriptors "^2.0.3" - -util@0.10.3, util@^0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= - dependencies: - inherits "2.0.1" - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= - -uuid@^3.0.0, uuid@^3.0.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" - integrity sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA== - -uuid@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== - -v8-compile-cache@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c" - integrity sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw== - -validate-npm-package-license@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" - integrity sha1-KAS6vnEq0zeUWaz74kdGqywwP7w= - dependencies: - spdx-correct "~1.0.0" - spdx-expression-parse "~1.0.0" - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - -vendors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22" - integrity sha1-N61zyO5Bf7PVgOeFMSMH0nSEfyI= - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -vm-browserify@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" - integrity sha1-XX6kW7755Kb/ZflUOOCofDV9WnM= - dependencies: - indexof "0.0.1" - -watchpack@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" - integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA== - dependencies: - chokidar "^2.0.2" - graceful-fs "^4.1.2" - neo-async "^2.5.0" - -wbuf@^1.1.0: - version "1.7.2" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.2.tgz#d697b99f1f59512df2751be42769c1580b5801fe" - integrity sha1-1pe5nx9ZUS3ydRvkJ2nBWAtYAf4= - dependencies: - minimalistic-assert "^1.0.0" - -wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - -webpack-assets-manifest@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/webpack-assets-manifest/-/webpack-assets-manifest-3.1.1.tgz#39bbc3bf2ee57fcd8ba07cda51c9ba4a3c6ae1de" - integrity sha512-JV9V2QKc5wEWQptdIjvXDUL1ucbPLH2f27toAY3SNdGZp+xSaStAgpoMcvMZmqtFrBc9a5pTS1058vxyMPOzRQ== - dependencies: - chalk "^2.0" - lodash.get "^4.0" - lodash.has "^4.0" - mkdirp "^0.5" - schema-utils "^1.0.0" - tapable "^1.0.0" - webpack-sources "^1.0.0" - -webpack-cli@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.2.3.tgz#13653549adfd8ccd920ad7be1ef868bacc22e346" - integrity sha512-Ik3SjV6uJtWIAN5jp5ZuBMWEAaP5E4V78XJ2nI+paFPh8v4HPSwo/myN0r29Xc/6ZKnd2IdrAlpSgNOu2CDQ6Q== - dependencies: - chalk "^2.4.1" - cross-spawn "^6.0.5" - enhanced-resolve "^4.1.0" - findup-sync "^2.0.0" - global-modules "^1.0.0" - import-local "^2.0.0" - interpret "^1.1.0" - loader-utils "^1.1.0" - supports-color "^5.5.0" - v8-compile-cache "^2.0.2" - yargs "^12.0.4" - -webpack-dev-middleware@^3.5.1: - version "3.6.1" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.6.1.tgz#91f2531218a633a99189f7de36045a331a4b9cd4" - integrity sha512-XQmemun8QJexMEvNFbD2BIg4eSKrmSIMrTfnl2nql2Sc6OGAYFyb8rwuYrCjl/IiEYYuyTEiimMscu7EXji/Dw== - dependencies: - memory-fs "^0.4.1" - mime "^2.3.1" - range-parser "^1.0.3" - webpack-log "^2.0.0" - -webpack-dev-server@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.2.1.tgz#1b45ce3ecfc55b6ebe5e36dab2777c02bc508c4e" - integrity sha512-sjuE4mnmx6JOh9kvSbPYw3u/6uxCLHNWfhWaIPwcXWsvWOPN+nc5baq4i9jui3oOBRXGonK9+OI0jVkaz6/rCw== - dependencies: - ansi-html "0.0.7" - bonjour "^3.5.0" - chokidar "^2.0.0" - compression "^1.5.2" - connect-history-api-fallback "^1.3.0" - debug "^4.1.1" - del "^3.0.0" - express "^4.16.2" - html-entities "^1.2.0" - http-proxy-middleware "^0.19.1" - import-local "^2.0.0" - internal-ip "^4.2.0" - ip "^1.1.5" - killable "^1.0.0" - loglevel "^1.4.1" - opn "^5.1.0" - portfinder "^1.0.9" - schema-utils "^1.0.0" - selfsigned "^1.9.1" - semver "^5.6.0" - serve-index "^1.7.2" - sockjs "0.3.19" - sockjs-client "1.3.0" - spdy "^4.0.0" - strip-ansi "^3.0.0" - supports-color "^6.1.0" - url "^0.11.0" - webpack-dev-middleware "^3.5.1" - webpack-log "^2.0.0" - yargs "12.0.2" - -webpack-log@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" - integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== - dependencies: - ansi-colors "^3.0.0" - uuid "^3.3.2" - -webpack-sources@^1.0.0, webpack-sources@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" - integrity sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - -webpack-sources@^1.0.1, webpack-sources@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54" - integrity sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - -webpack@^4.29.6: - version "4.29.6" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.29.6.tgz#66bf0ec8beee4d469f8b598d3988ff9d8d90e955" - integrity sha512-MwBwpiE1BQpMDkbnUUaW6K8RFZjljJHArC6tWQJoFm0oQtfoSebtg4Y7/QHnJ/SddtjYLHaKGX64CFjG5rehJw== - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-module-context" "1.8.5" - "@webassemblyjs/wasm-edit" "1.8.5" - "@webassemblyjs/wasm-parser" "1.8.5" - acorn "^6.0.5" - acorn-dynamic-import "^4.0.0" - ajv "^6.1.0" - ajv-keywords "^3.1.0" - chrome-trace-event "^1.0.0" - enhanced-resolve "^4.1.0" - eslint-scope "^4.0.0" - json-parse-better-errors "^1.0.2" - loader-runner "^2.3.0" - loader-utils "^1.1.0" - memory-fs "~0.4.1" - micromatch "^3.1.8" - mkdirp "~0.5.0" - neo-async "^2.5.0" - node-libs-browser "^2.0.0" - schema-utils "^1.0.0" - tapable "^1.1.0" - terser-webpack-plugin "^1.1.0" - watchpack "^1.5.0" - webpack-sources "^1.3.0" - -websocket-driver@>=0.5.1: - version "0.7.0" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb" - integrity sha1-DK+dLXVdk67gSdS90NP+LMoqJOs= - dependencies: - http-parser-js ">=0.4.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" - integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== - -which-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" - integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@1, which@^1.2.9: - version "1.3.0" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" - integrity sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg== - dependencies: - isexe "^2.0.0" - -which@^1.2.14: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" - integrity sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w== - dependencies: - string-width "^1.0.2" - -worker-farm@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae" - integrity sha512-XxiQ9kZN5n6mmnW+mFJ+wXjNNI/Nx4DIdaAKLX1Bn6LYBWlN/zaBhu34DQYPZ1AJobQuu67S2OfDdNSVULvXkQ== - dependencies: - errno "^0.1.4" - xtend "^4.0.1" - -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -xregexp@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020" - integrity sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg== - -xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" - integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= - -y18n@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= - -"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - -yallist@^3.0.0, yallist@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" - integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== - -yargs-parser@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== - dependencies: - camelcase "^4.1.0" - -yargs-parser@^11.1.1: - version "11.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" - integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" - integrity sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo= - dependencies: - camelcase "^3.0.0" - -yargs@12.0.2: - version "12.0.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.2.tgz#fe58234369392af33ecbef53819171eff0f5aadc" - integrity sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ== - dependencies: - cliui "^4.0.0" - decamelize "^2.0.0" - find-up "^3.0.0" - get-caller-file "^1.0.1" - os-locale "^3.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1 || ^4.0.0" - yargs-parser "^10.1.0" - -yargs@^12.0.4: - version "12.0.5" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" - integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== - dependencies: - cliui "^4.0.0" - decamelize "^1.2.0" - find-up "^3.0.0" - get-caller-file "^1.0.1" - os-locale "^3.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1 || ^4.0.0" - yargs-parser "^11.1.1" - -yargs@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" - integrity sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg= - dependencies: - camelcase "^3.0.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.2" - which-module "^1.0.0" - y18n "^3.2.1" - yargs-parser "^5.0.0" diff --git a/actionmailbox/test/fixtures/files/invalid_utf.eml b/actionmailbox/test/fixtures/files/invalid_utf.eml new file mode 100644 index 0000000000000..c5c03d572b643 --- /dev/null +++ b/actionmailbox/test/fixtures/files/invalid_utf.eml @@ -0,0 +1,39 @@ +thread-index: Adjkg/rniynGRZvvRu2Ftd4zu7/YrA== +Thread-Topic: =?iso-8859-2?Q?Informace_o_skladov=FDch_z=E1sob=E1ch_Copmany?= +From: +To: , +Message-ID: <05988AA6EC0D44318855A5E39E3B6F9E@jansterba.com> +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_168F_01D8E494.BE7019A0" +Content-Class: urn:content-classes:message +Importance: normal +Priority: normal +X-MimeOLE: Produced By Microsoft MimeOLE V6.3.9600.20564 +X-EOPAttributedMessage: 0 +X-Spam-IndexStatus: 0 + +This is a multi-part message in MIME format. + +------=_NextPart_000_168F_01D8E494.BE7019A0 +Content-Type: multipart/alternative; + boundary="----=_NextPart_001_1690_01D8E494.BE7019A0" + +------=_NextPart_001_1690_01D8E494.BE7019A0 +Content-Type: text/plain; + charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + +V=E1=BEen=FD z=E1kazn=EDku, + +v p=F8=EDloze zas=EDl=E1me aktu=E1ln=ED informace o skladov=FDch = +z=E1sob=E1ch. + +------=_NextPart_001_1690_01D8E494.BE7019A0 +Content-Type: text/html; + charset="iso-8859-2" +Content-Transfer-Encoding: 8bit + +Vá¾ený zákazníku,

v pøíloze zasíláme aktuální informace o skladových zásobách. + +------=_NextPart_000_168F_01D8E494.BE7019A0-- diff --git a/actionmailbox/test/fixtures/files/welcome.eml b/actionmailbox/test/fixtures/files/welcome.eml index 5d6b3c1ea5296..27fd51c58a586 100644 --- a/actionmailbox/test/fixtures/files/welcome.eml +++ b/actionmailbox/test/fixtures/files/welcome.eml @@ -27,7 +27,7 @@ Content-Type: multipart/related; Content-Transfer-Encoding: base64 Content-Disposition: inline; filename=avatar1.jpeg -Content-Type: image/jpg; +Content-Type: image/jpeg; name="avatar1.jpeg" Content-Id: <7AAEB353-2341-4D46-A054-5CA5CB2363B7> @@ -397,7 +397,7 @@ mk8VWW5WRGJGAOaAP//Z Content-Transfer-Encoding: base64 Content-Disposition: inline; filename=avatar2.jpg -Content-Type: image/jpg; +Content-Type: image/jpeg; x-unix-mode=0700; name="avatar2.jpg" Content-Id: <4594E827-6E69-4329-8691-6BC35E3E73A0> diff --git a/actionmailbox/test/generators/mailbox_generator_test.rb b/actionmailbox/test/generators/mailbox_generator_test.rb index 9ffa73321b0e6..f9677ba591270 100644 --- a/actionmailbox/test/generators/mailbox_generator_test.rb +++ b/actionmailbox/test/generators/mailbox_generator_test.rb @@ -49,7 +49,7 @@ def test_mailbox_skeleton_is_created_with_namespace end def test_check_class_collision - Object.send :const_set, :InboxMailbox, Class.new + Object.const_set :InboxMailbox, Class.new content = capture(:stderr) { run_generator } assert_match(/The name 'InboxMailbox' is either already used in your application or reserved/, content) ensure diff --git a/actionmailbox/test/jobs/incineration_job_test.rb b/actionmailbox/test/jobs/incineration_job_test.rb index a3907898abbb0..03948a32bf93f 100644 --- a/actionmailbox/test/jobs/incineration_job_test.rb +++ b/actionmailbox/test/jobs/incineration_job_test.rb @@ -3,7 +3,7 @@ require "test_helper" class ActionMailbox::IncinerationJobTest < ActiveJob::TestCase - setup { @inbound_email = receive_inbound_email_from_fixture("welcome.eml") } + setup { @inbound_email = create_inbound_email_from_fixture("welcome.eml") } test "ignoring a missing inbound email" do @inbound_email.destroy! diff --git a/actionmailbox/test/migrations_test.rb b/actionmailbox/test/migrations_test.rb new file mode 100644 index 0000000000000..eb18781e03081 --- /dev/null +++ b/actionmailbox/test/migrations_test.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "test_helper" +require ActionMailbox::Engine.root.join("db/migrate/20180917164000_create_action_mailbox_tables.rb").to_s + +class ActionMailbox::MigrationsTest < ActiveSupport::TestCase + setup do + @original_verbose = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + @connection = ActiveRecord::Base.lease_connection + @original_options = Rails.configuration.generators.options.deep_dup + end + + teardown do + Rails.configuration.generators.options = @original_options + rerun_migration + ActiveRecord::Migration.verbose = @original_verbose + end + + test "migration creates tables with default primary key type" do + action_mailbox_tables.each do |table| + assert_equal :integer, primary_key(table).type + end + end + + test "migration creates tables with configured primary key type" do + Rails.configuration.generators do |g| + g.orm :active_record, primary_key_type: :string + end + + rerun_migration + + action_mailbox_tables.each do |table| + assert_equal :string, primary_key(table).type + end + end + + private + def rerun_migration + CreateActionMailboxTables.migrate(:down) + CreateActionMailboxTables.migrate(:up) + end + + def action_mailbox_tables + @action_mailbox_tables ||= ActionMailbox::Record.descendants.map { |klass| klass.table_name.to_sym } + end + + def primary_key(table) + @connection.columns(table).find { |c| c.name == "id" } + end +end diff --git a/actionmailbox/test/models/table_name_test.rb b/actionmailbox/test/models/table_name_test.rb new file mode 100644 index 0000000000000..0b082d1bd4867 --- /dev/null +++ b/actionmailbox/test/models/table_name_test.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "test_helper" + +class ActionMailbox::TableNameTest < ActiveSupport::TestCase + setup do + @old_prefix = ActiveRecord::Base.table_name_prefix + @old_suffix = ActiveRecord::Base.table_name_suffix + + ActiveRecord::Base.table_name_prefix = @prefix = "abc_" + ActiveRecord::Base.table_name_suffix = @suffix = "_xyz" + + @models = [ActionMailbox::InboundEmail] + @models.map(&:reset_table_name) + end + + teardown do + ActiveRecord::Base.table_name_prefix = @old_prefix + ActiveRecord::Base.table_name_suffix = @old_suffix + + @models.map(&:reset_table_name) + end + + test "prefix and suffix are added to the Action Mailbox tables' name" do + assert_equal( + "#{@prefix}action_mailbox_inbound_emails#{@suffix}", + ActionMailbox::InboundEmail.table_name + ) + end +end diff --git a/actionmailbox/test/test_helper.rb b/actionmailbox/test/test_helper.rb index 09f6cc818dc5f..2f4edb8282d49 100644 --- a/actionmailbox/test/test_helper.rb +++ b/actionmailbox/test/test_helper.rb @@ -1,24 +1,23 @@ # frozen_string_literal: true +require_relative "../../tools/strict_warnings" + ENV["RAILS_ENV"] = "test" ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL" require_relative "../test/dummy/config/environment" -ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] +ActiveRecord::Migrator.migrations_paths = [ File.expand_path("../test/dummy/db/migrate", __dir__) ] require "rails/test_help" -require "byebug" require "webmock/minitest" -Minitest.backtrace_filter = Minitest::BacktraceFilter.new - require "rails/test_unit/reporter" Rails::TestUnitReporter.executable = "bin/test" -if ActiveSupport::TestCase.respond_to?(:fixture_path=) - ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) - ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path - ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" +if ActiveSupport::TestCase.respond_to?(:fixture_paths=) + ActiveSupport::TestCase.fixture_paths = [File.expand_path("fixtures", __dir__)] + ActionDispatch::IntegrationTest.fixture_paths = ActiveSupport::TestCase.fixture_paths + ActiveSupport::TestCase.file_fixture_path = File.expand_path("fixtures", __dir__) + "/files" ActiveSupport::TestCase.fixtures :all end diff --git a/actionmailbox/test/unit/inbound_email_test.rb b/actionmailbox/test/unit/inbound_email_test.rb index 76f047eb61c9d..264209c118192 100644 --- a/actionmailbox/test/unit/inbound_email_test.rb +++ b/actionmailbox/test/unit/inbound_email_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "../test_helper" +require "minitest/mock" module ActionMailbox class InboundEmailTest < ActiveSupport::TestCase @@ -33,5 +34,51 @@ class InboundEmailTest < ActiveSupport::TestCase inbound_email_2 = create_inbound_email_from_source(mail.to_s) assert_nil inbound_email_2 end + + test "error on upload doesn't leave behind a pending inbound email" do + ActiveStorage::Blob.service.stub(:upload, -> { raise "Boom!" }) do + assert_no_difference -> { ActionMailbox::InboundEmail.count } do + assert_raises do + create_inbound_email_from_fixture "welcome.eml" + end + end + end + end + + test "email gets saved to the configured storage service" do + ActionMailbox.storage_service = :test_email + + assert_equal(:test_email, ActionMailbox.storage_service) + + email = create_inbound_email_from_fixture("welcome.eml") + + storage_service = ActiveStorage::Blob.services.fetch(ActionMailbox.storage_service) + raw = email.raw_email_blob + + # Not present in the main storage + assert_not(ActiveStorage::Blob.service.exist?(raw.key)) + # Present in the email storage + assert(storage_service.exist?(raw.key)) + ensure + ActionMailbox.storage_service = nil + end + + test "email gets saved to the default storage service, even if it gets changed" do + default_service = ActiveStorage::Blob.service + ActiveStorage::Blob.service = ActiveStorage::Blob.services.fetch(:test_email) + + # Doesn't change ActionMailbox.storage_service + assert_nil(ActionMailbox.storage_service) + + email = create_inbound_email_from_fixture("welcome.eml") + raw = email.raw_email_blob + + # Not present in the (previously) default storage + assert_not(default_service.exist?(raw.key)) + # Present in the current default storage (email) + assert(ActiveStorage::Blob.service.exist?(raw.key)) + ensure + ActiveStorage::Blob.service = default_service + end end end diff --git a/actionmailbox/test/unit/mail_ext/addresses_test.rb b/actionmailbox/test/unit/mail_ext/addresses_test.rb index 0758921d269f5..92ea75a071ccf 100644 --- a/actionmailbox/test/unit/mail_ext/addresses_test.rb +++ b/actionmailbox/test/unit/mail_ext/addresses_test.rb @@ -7,18 +7,24 @@ class AddressesTest < ActiveSupport::TestCase setup do @mail = Mail.new \ from: "sally@example.com", + reply_to: "sarah@example.com", to: "david@basecamp.com", cc: "jason@basecamp.com", bcc: "andrea@basecamp.com", - x_original_to: "ryan@basecamp.com" + x_original_to: "ryan@basecamp.com", + x_forwarded_to: "jane@example.com" end test "from address uses address object" do assert_equal "example.com", @mail.from_address.domain end - test "recipients include everyone from to, cc, bcc, and x-original-to" do - assert_equal %w[ david@basecamp.com jason@basecamp.com andrea@basecamp.com ryan@basecamp.com ], @mail.recipients + test "reply to address uses address object" do + assert_equal "example.com", @mail.reply_to_address.domain + end + + test "recipients include everyone from to, cc, bcc, x-original-to, and x-forwarded-to" do + assert_equal %w[ david@basecamp.com jason@basecamp.com andrea@basecamp.com ryan@basecamp.com jane@example.com ], @mail.recipients end test "recipients addresses use address objects" do @@ -40,5 +46,9 @@ class AddressesTest < ActiveSupport::TestCase test "x_original_to addresses use address objects" do assert_equal "basecamp.com", @mail.x_original_to_addresses.first.domain end + + test "x_forwarded_to addresses use address objects" do + assert_equal "example.com", @mail.x_forwarded_to_addresses.first.domain + end end end diff --git a/actionmailbox/test/unit/mailbox/bouncing_test.rb b/actionmailbox/test/unit/mailbox/bouncing_test.rb index d4bd6ea6db72e..667ec62c04c31 100644 --- a/actionmailbox/test/unit/mailbox/bouncing_test.rb +++ b/actionmailbox/test/unit/mailbox/bouncing_test.rb @@ -8,6 +8,12 @@ def process end end +class BouncingWithImmediateReplyMailbox < ActionMailbox::Base + def process + bounce_now_with BounceMailer.bounce(to: mail.from) + end +end + class ActionMailbox::Base::BouncingTest < ActiveSupport::TestCase include ActionMailer::TestHelper @@ -16,12 +22,29 @@ class ActionMailbox::Base::BouncingTest < ActiveSupport::TestCase from: "sender@example.com", to: "replies@example.com", subject: "Bounce me" end + teardown do + ActionMailer::Base.deliveries.clear + end + test "bouncing with a reply" do perform_enqueued_jobs only: ActionMailer::MailDeliveryJob do BouncingWithReplyMailbox.receive @inbound_email end - assert @inbound_email.bounced? + assert_predicate @inbound_email, :bounced? + assert_emails 1 + + mail = ActionMailer::Base.deliveries.last + assert_equal %w[ sender@example.com ], mail.to + assert_equal "Your email was not delivered", mail.subject + end + + test "bouncing now with a reply" do + assert_no_enqueued_emails do + BouncingWithImmediateReplyMailbox.receive @inbound_email + end + + assert_predicate @inbound_email, :bounced? assert_emails 1 mail = ActionMailer::Base.deliveries.last diff --git a/actionmailbox/test/unit/mailbox/callbacks_test.rb b/actionmailbox/test/unit/mailbox/callbacks_test.rb index 8d98a3f3acd40..1917aa3250da0 100644 --- a/actionmailbox/test/unit/mailbox/callbacks_test.rb +++ b/actionmailbox/test/unit/mailbox/callbacks_test.rb @@ -61,7 +61,7 @@ class ActionMailbox::Base::CallbacksTest < ActiveSupport::TestCase test "bouncing in a callback terminates processing" do BouncingCallbackMailbox.receive @inbound_email - assert @inbound_email.bounced? + assert_predicate @inbound_email, :bounced? assert_equal [ "Pre-bounce", "Bounce" ], $before_processing assert_not $processed assert_not $after_processing @@ -69,7 +69,7 @@ class ActionMailbox::Base::CallbacksTest < ActiveSupport::TestCase test "marking the inbound email as delivered in a callback terminates processing" do DiscardingCallbackMailbox.receive @inbound_email - assert @inbound_email.delivered? + assert_predicate @inbound_email, :delivered? assert_equal [ "Pre-discard", "Discard" ], $before_processing assert_not $processed assert_not $after_processing diff --git a/actionmailbox/test/unit/mailbox/notifications_test.rb b/actionmailbox/test/unit/mailbox/notifications_test.rb new file mode 100644 index 0000000000000..655ed4560258f --- /dev/null +++ b/actionmailbox/test/unit/mailbox/notifications_test.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative "../../test_helper" + +class RepliesMailbox < ActionMailbox::Base +end + +class ActionMailbox::Base::NotificationsTest < ActiveSupport::TestCase + test "instruments processing" do + mailbox = RepliesMailbox.new(create_inbound_email_from_fixture("welcome.eml")) + expected_payload = { + mailbox:, + inbound_email: { + id: 1, + message_id: "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", + status: "processing" + } + } + + assert_notifications_count("process.action_mailbox", 1) do + assert_notification("process.action_mailbox", expected_payload) do + mailbox.perform_processing + end + end + end +end diff --git a/actionmailbox/test/unit/mailbox/state_test.rb b/actionmailbox/test/unit/mailbox/state_test.rb index b3a58ad667667..cf1fd5441ea72 100644 --- a/actionmailbox/test/unit/mailbox/state_test.rb +++ b/actionmailbox/test/unit/mailbox/state_test.rb @@ -33,19 +33,19 @@ class ActionMailbox::Base::StateTest < ActiveSupport::TestCase test "successful mailbox processing leaves inbound email in delivered state" do SuccessfulMailbox.receive @inbound_email - assert @inbound_email.delivered? + assert_predicate @inbound_email, :delivered? assert_equal "I was processed", $processed end test "unsuccessful mailbox processing leaves inbound email in failed state" do UnsuccessfulMailbox.receive @inbound_email - assert @inbound_email.failed? + assert_predicate @inbound_email, :failed? assert_equal :failure, $processed end test "bounced inbound emails are not delivered" do BouncingMailbox.receive @inbound_email - assert @inbound_email.bounced? + assert_predicate @inbound_email, :bounced? assert_equal :bounced, $processed end end diff --git a/actionmailbox/test/unit/relayer_test.rb b/actionmailbox/test/unit/relayer_test.rb index fb2b48ea16b6b..89701d0baa111 100644 --- a/actionmailbox/test/unit/relayer_test.rb +++ b/actionmailbox/test/unit/relayer_test.rb @@ -19,7 +19,7 @@ class RelayerTest < ActiveSupport::TestCase result = @relayer.relay(file_fixture("welcome.eml").read) assert_equal "2.0.0", result.status_code assert_equal "Successfully relayed message to ingress", result.message - assert result.success? + assert_predicate result, :success? assert_not result.failure? assert_requested :post, URL, body: file_fixture("welcome.eml").read, @@ -34,7 +34,7 @@ class RelayerTest < ActiveSupport::TestCase assert_equal "4.7.0", result.status_code assert_equal "Invalid credentials for ingress", result.message assert_not result.success? - assert result.failure? + assert_predicate result, :failure? end test "unsuccessfully relaying due to an unspecified server error" do @@ -44,7 +44,7 @@ class RelayerTest < ActiveSupport::TestCase assert_equal "4.0.0", result.status_code assert_equal "HTTP 500", result.message assert_not result.success? - assert result.failure? + assert_predicate result, :failure? end test "unsuccessfully relaying due to a gateway timeout" do @@ -54,7 +54,7 @@ class RelayerTest < ActiveSupport::TestCase assert_equal "4.0.0", result.status_code assert_equal "HTTP 504", result.message assert_not result.success? - assert result.failure? + assert_predicate result, :failure? end test "unsuccessfully relaying due to ECONNRESET" do @@ -64,7 +64,7 @@ class RelayerTest < ActiveSupport::TestCase assert_equal "4.4.2", result.status_code assert_equal "Network error relaying to ingress: Connection reset by peer", result.message assert_not result.success? - assert result.failure? + assert_predicate result, :failure? end test "unsuccessfully relaying due to connection failure" do @@ -74,7 +74,7 @@ class RelayerTest < ActiveSupport::TestCase assert_equal "4.4.2", result.status_code assert_equal "Network error relaying to ingress: Failed to open TCP connection to example.com:443", result.message assert_not result.success? - assert result.failure? + assert_predicate result, :failure? end test "unsuccessfully relaying due to client-side timeout" do @@ -84,7 +84,7 @@ class RelayerTest < ActiveSupport::TestCase assert_equal "4.4.2", result.status_code assert_equal "Timed out relaying to ingress", result.message assert_not result.success? - assert result.failure? + assert_predicate result, :failure? end test "unsuccessfully relaying due to an unhandled exception" do @@ -94,7 +94,7 @@ class RelayerTest < ActiveSupport::TestCase assert_equal "4.0.0", result.status_code assert_equal "Error relaying to ingress: Something went wrong", result.message assert_not result.success? - assert result.failure? + assert_predicate result, :failure? end end end diff --git a/actionmailbox/test/unit/router_test.rb b/actionmailbox/test/unit/router_test.rb index 7eb8e04a734c4..d1f46c18c8bdd 100644 --- a/actionmailbox/test/unit/router_test.rb +++ b/actionmailbox/test/unit/router_test.rb @@ -132,17 +132,18 @@ class RouterTest < ActiveSupport::TestCase end test "missing route" do + inbound_email = create_inbound_email_from_mail(to: "going-nowhere@example.com", subject: "This is a reply") assert_raises(ActionMailbox::Router::RoutingError) do - inbound_email = create_inbound_email_from_mail(to: "going-nowhere@example.com", subject: "This is a reply") @router.route inbound_email - assert inbound_email.bounced? end + assert_predicate inbound_email, :bounced? end test "invalid address" do - assert_raises(ArgumentError) do + error = assert_raises(ArgumentError) do @router.add_route Array.new, to: :first end + assert_equal "Expected a Symbol, String, Regexp, Proc, or matchable, got []", error.message end test "single string mailbox_for" do diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index c6ebfdaadcb92..96299d1fedd08 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,10 +1,2 @@ -* Fix ActionMailer assertions don't work for parameterized mail with legacy delivery job. - *bogdanvlviv* - -* Added `email_address_with_name` to properly escape addresses with names. - - *Sunny Ripert* - - -Please check [6-0-stable](https://github.com/rails/rails/blob/6-0-stable/actionmailer/CHANGELOG.md) for previous changes. +Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/actionmailer/CHANGELOG.md) for previous changes. diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE index 0e2be7cd7f0d7..7be9ac633faf0 100644 --- a/actionmailer/MIT-LICENSE +++ b/actionmailer/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2020 David Heinemeier Hansson +Copyright (c) David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index f9c84b4232c8f..d26365048b4f7 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -13,7 +13,7 @@ Additionally, an Action Mailer class can be used to process incoming email, such as allowing a blog to accept new posts from an email (which could even have been sent from a phone). -You can read more about Action Mailer in the {Action Mailer Basics}[https://edgeguides.rubyonrails.org/action_mailer_basics.html] guide. +You can read more about Action Mailer in the {Action Mailer Basics}[https://guides.rubyonrails.org/action_mailer_basics.html] guide. == Sending emails @@ -78,7 +78,7 @@ Or you can just chain the methods together like: It is possible to set default values that will be used in every method in your Action Mailer class. To implement this functionality, you just call the public -class method +default+ which you get for free from ActionMailer::Base. +class method +default+ which you get for free from ActionMailer::Base. This method accepts a Hash as the parameter. You can use any of the headers, email messages have, like +:from+ as the key. You can also pass in a string as the key, like "Content-Type", but Action Mailer does this out of the box for you, @@ -114,9 +114,9 @@ The latest version of Action Mailer can be installed with RubyGems: $ gem install actionmailer -Source code can be downloaded as part of the Rails project on GitHub: +Source code can be downloaded as part of the \Rails project on GitHub: -* https://github.com/rails/rails/tree/master/actionmailer +* https://github.com/rails/rails/tree/main/actionmailer == License @@ -132,7 +132,7 @@ API documentation is at * https://api.rubyonrails.org -Bug reports for the Ruby on Rails project can be filed here: +Bug reports for the Ruby on \Rails project can be filed here: * https://github.com/rails/rails/issues diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile index 6ac408e1cbf03..8c6181e659337 100644 --- a/actionmailer/Rakefile +++ b/actionmailer/Rakefile @@ -5,7 +5,7 @@ require "rake/testtask" desc "Default Task" task default: [ :test ] -task :package +ENV["RAILS_MINITEST_PLUGIN"] = "true" # Run the unit tests Rake::TestTask.new { |t| @@ -13,6 +13,7 @@ Rake::TestTask.new { |t| t.pattern = "test/**/*_test.rb" t.warning = true t.verbose = true + t.options = "--profile" if ENV["CI"] t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) } diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 4d01a6fbda749..88a9be08c1cf7 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.summary = "Email composition and delivery framework (part of Rails)." s.description = "Email on Rails. Compose, deliver, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments." - s.required_ruby_version = ">= 2.5.0" + s.required_ruby_version = ">= 3.2.0" s.license = "MIT" @@ -27,6 +27,7 @@ Gem::Specification.new do |s| "documentation_uri" => "https://api.rubyonrails.org/v#{version}/", "mailing_list_uri" => "https://discuss.rubyonrails.org/c/rubyonrails-talk", "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actionmailer", + "rubygems_mfa_required" => "true", } # NOTE: Please read our dependency guidelines before updating versions: @@ -37,6 +38,6 @@ Gem::Specification.new do |s| s.add_dependency "actionview", version s.add_dependency "activejob", version - s.add_dependency "mail", ["~> 2.5", ">= 2.5.4"] - s.add_dependency "rails-dom-testing", "~> 2.0" + s.add_dependency "mail", ">= 2.8.0" + s.add_dependency "rails-dom-testing", "~> 2.2" end diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 426ca919a1c5f..f58a858094143 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2020 David Heinemeier Hansson +# Copyright (c) David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -25,6 +25,7 @@ require "abstract_controller" require "action_mailer/version" +require "action_mailer/deprecator" # Common Active Support usage in Action Mailer require "active_support" @@ -34,6 +35,7 @@ require "active_support/core_ext/string/inflections" require "active_support/lazy_load_hooks" +# :include: ../README.rdoc module ActionMailer extend ::ActiveSupport::Autoload @@ -42,6 +44,7 @@ module ActionMailer end autoload :Base + autoload :Callbacks autoload :DeliveryMethods autoload :InlinePreviewInterceptor autoload :MailHelper @@ -51,14 +54,19 @@ module ActionMailer autoload :TestCase autoload :TestHelper autoload :MessageDelivery - autoload :DeliveryJob autoload :MailDeliveryJob + autoload :QueuedDelivery + autoload :FormBuilder def self.eager_load! super require "mail" Mail.eager_autoload! + + Base.descendants.each do |mailer| + mailer.eager_load! unless mailer.abstract? + end end end @@ -66,5 +74,6 @@ def self.eager_load! ActiveSupport.on_load(:action_view) do ActionView::Base.default_formats ||= Mime::SET.symbols - ActionView::Template::Types.delegate_to Mime + ActionView::Template.mime_types_implementation = Mime + ActionView::LookupContext::DetailsKey.clear end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index ed7c55566c864..bc3e8f306ba52 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -10,16 +10,18 @@ require "action_mailer/rescuable" module ActionMailer + # = Action Mailer \Base + # # Action Mailer allows you to send email from your application using a mailer model and views. # - # = Mailer Models + # == Mailer Models # # To use Action Mailer, you need to create a mailer model. # # $ bin/rails generate mailer Notifier # # The generated model inherits from ApplicationMailer which in turn - # inherits from ActionMailer::Base. A mailer model defines methods + # inherits from +ActionMailer::Base+. A mailer model defines methods # used to generate an email message. In these methods, you can set up variables to be used in # the mailer views, options on the mail itself such as the :from address, and attachments. # @@ -56,7 +58,7 @@ module ActionMailer # # * mail - Allows you to specify email to be sent. # - # The hash passed to the mail method allows you to specify any header that a Mail::Message + # The hash passed to the mail method allows you to specify any header that a +Mail::Message+ # will accept (any valid email header including optional fields). # # The +mail+ method, if not passed a block, will inspect your views and send all the views with @@ -84,7 +86,7 @@ module ActionMailer # format.html { render "some_other_template" } # end # - # = Mailer views + # == Mailer views # # Like Action Controller, each mailer class has a corresponding view directory in which each # method of the class looks for a template with its name. @@ -106,13 +108,13 @@ module ActionMailer # You got a new note! # <%= truncate(@note.body, length: 25) %> # - # If you need to access the subject, from or the recipients in the view, you can do that through message object: + # If you need to access the subject, from, or the recipients in the view, you can do that through message object: # # You got a new note from <%= message.from %>! # <%= truncate(@note.body, length: 25) %> # # - # = Generating URLs + # == Generating URLs # # URLs can be generated in mailer views using url_for or named routes. Unlike controllers from # Action Pack, the mailer instance doesn't have any context about the incoming request, so you'll need @@ -140,7 +142,7 @@ module ActionMailer # # By default when config.force_ssl is +true+, URLs generated for hosts will use the HTTPS protocol. # - # = Sending mail + # == Sending mail # # Once a mailer action and template are defined, you can deliver your message or defer its creation and # delivery for later: @@ -149,9 +151,9 @@ module ActionMailer # mail = NotifierMailer.welcome(User.first) # => an ActionMailer::MessageDelivery object # mail.deliver_now # generates and sends the email now # - # The ActionMailer::MessageDelivery class is a wrapper around a delegate that will call - # your method to generate the mail. If you want direct access to the delegator, or Mail::Message, - # you can call the message method on the ActionMailer::MessageDelivery object. + # The ActionMailer::MessageDelivery class is a wrapper around a delegate that will call + # your method to generate the mail. If you want direct access to the delegator, or +Mail::Message+, + # you can call the message method on the ActionMailer::MessageDelivery object. # # NotifierMailer.welcome(User.first).message # => a Mail::Message object # @@ -165,7 +167,7 @@ module ActionMailer # You never instantiate your mailer class. Rather, you just call the method you defined on the class itself. # All instance methods are expected to return a message object to be sent. # - # = Multipart Emails + # == Multipart Emails # # Multipart messages can also be used implicitly because Action Mailer will automatically detect and use # multipart templates, where each template is named after the name of the action, followed by the content @@ -186,7 +188,7 @@ module ActionMailer # This means that you'll have to manually add each part to the email and set the content type of the email # to multipart/alternative. # - # = Attachments + # == Attachments # # Sending attachment in emails is easy: # @@ -213,7 +215,7 @@ module ActionMailer # end # end # - # You can also send attachments with html template, in this case you need to add body, attachments, + # You can also send attachments with HTML template, in this case you need to add body, attachments, # and custom content type like this: # # class NotifierMailer < ApplicationMailer @@ -226,7 +228,7 @@ module ActionMailer # end # end # - # = Inline Attachments + # == Inline Attachments # # You can also specify that a file should be displayed inline with other HTML. This is useful # if you want to display a corporate logo or a photo. @@ -252,7 +254,7 @@ module ActionMailer # # <%= image_tag attachments['photo.png'].url, alt: 'Our Photo', class: 'photo' -%> # - # = Observing and Intercepting Mails + # == Observing and Intercepting Mails # # Action Mailer provides hooks into the Mail observer and interceptor methods. These allow you to # register classes that are called during the mail delivery life cycle. @@ -263,9 +265,9 @@ module ActionMailer # An interceptor class must implement the :delivering_email(message) method which will be # called before the email is sent, allowing you to make modifications to the email before it hits # the delivery agents. Your class should make any needed modifications directly to the passed - # in Mail::Message instance. + # in +Mail::Message+ instance. # - # = Default Hash + # == Default \Hash # # Action Mailer provides some intelligent defaults for your emails, these are usually specified in a # default method inside the class definition: @@ -274,15 +276,15 @@ module ActionMailer # default sender: 'system@example.com' # end # - # You can pass in any header value that a Mail::Message accepts. Out of the box, - # ActionMailer::Base sets the following: + # You can pass in any header value that a +Mail::Message+ accepts. Out of the box, + # +ActionMailer::Base+ sets the following: # # * mime_version: "1.0" # * charset: "UTF-8" # * content_type: "text/plain" # * parts_order: [ "text/plain", "text/enriched", "text/html" ] # - # parts_order and charset are not actually valid Mail::Message header fields, + # parts_order and charset are not actually valid +Mail::Message+ header fields, # but Action Mailer translates them appropriately and sets the correct values. # # As you can pass in any header, you need to either quote the header as a string, or pass it in as @@ -314,14 +316,16 @@ module ActionMailer # # config.action_mailer.default_options = { from: "no-reply@example.org" } # - # = Callbacks + # == \Callbacks # - # You can specify callbacks using before_action and after_action for configuring your messages. - # This may be useful, for example, when you want to add default inline attachments for all - # messages sent out by a certain mailer class: + # You can specify callbacks using before_action and after_action for configuring your messages, + # and using before_deliver and after_deliver for wrapping the delivery process. + # For example, when you want to add default inline attachments and log delivery for all messages + # sent out by a certain mailer class: # # class NotifierMailer < ApplicationMailer # before_action :add_inline_attachment! + # after_deliver :log_delivery # # def welcome # mail @@ -331,21 +335,48 @@ module ActionMailer # def add_inline_attachment! # attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg') # end + # + # def log_delivery + # Rails.logger.info "Sent email with message id '#{message.message_id}' at #{Time.current}." + # end # end # - # Callbacks in Action Mailer are implemented using - # AbstractController::Callbacks, so you can define and configure + # Action callbacks in Action Mailer are implemented using + # AbstractController::Callbacks, so you can define and configure # callbacks in the same manner that you would use callbacks in classes that - # inherit from ActionController::Base. + # inherit from ActionController::Base. # # Note that unless you have a specific reason to do so, you should prefer # using before_action rather than after_action in your # Action Mailer classes so that headers are parsed properly. # - # = Previewing emails + # == Rescuing Errors + # + # +rescue+ blocks inside of a mailer method cannot rescue errors that occur + # outside of rendering -- for example, record deserialization errors in a + # background job, or errors from a third-party mail delivery service. + # + # To rescue errors that occur during any part of the mailing process, use + # {rescue_from}[rdoc-ref:ActiveSupport::Rescuable::ClassMethods#rescue_from]: + # + # class NotifierMailer < ApplicationMailer + # rescue_from ActiveJob::DeserializationError do + # # ... + # end + # + # rescue_from "SomeThirdPartyService::ApiError" do + # # ... + # end + # + # def notify(recipient) + # mail(to: recipient, subject: "Notification") + # end + # end + # + # == Previewing emails # # You can preview your email templates visually by adding a mailer preview file to the - # ActionMailer::Base.preview_path. Since most emails do something interesting + # ActionMailer::Base.preview_paths. Since most emails do something interesting # with database data, you'll need to write some scenarios to load messages with fake data: # # class NotifierMailerPreview < ActionMailer::Preview @@ -354,12 +385,12 @@ module ActionMailer # end # end # - # Methods must return a Mail::Message object which can be generated by calling the mailer + # Methods must return a +Mail::Message+ object which can be generated by calling the mailer # method without the additional deliver_now / deliver_later. The location of the - # mailer previews directory can be configured using the preview_path option which has a default + # mailer preview directories can be configured using the preview_paths option which has a default # of test/mailers/previews: # - # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews" + # config.action_mailer.preview_paths << "#{Rails.root}/lib/mailer_previews" # # An overview of all previews is accessible at http://localhost:3000/rails/mailers # on a running development server instance. @@ -379,7 +410,7 @@ module ActionMailer # and register_preview_interceptor if they should operate on both sending and # previewing emails. # - # = Configuration options + # == Configuration options # # These options are specified on the class level, like # ActionMailer::Base.raise_delivery_errors = true @@ -402,17 +433,21 @@ module ActionMailer # This is a symbol and one of :plain (will send the password Base64 encoded), :login (will # send the password Base64 encoded) or :cram_md5 (combines a Challenge/Response mechanism to exchange # information and a cryptographic Message Digest 5 algorithm to hash important information) + # * :enable_starttls - Use STARTTLS when connecting to your SMTP server and fail if unsupported. Defaults + # to false. Requires at least version 2.7 of the Mail gem. # * :enable_starttls_auto - Detects if STARTTLS is enabled in your SMTP server and starts # to use it. Defaults to true. # * :openssl_verify_mode - When using TLS, you can set how OpenSSL checks the certificate. This is # really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name # of an OpenSSL verify constant ('none' or 'peer') or directly the constant - # (OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER). + # (+OpenSSL::SSL::VERIFY_NONE+ or +OpenSSL::SSL::VERIFY_PEER+). # * :ssl/:tls Enables the SMTP connection to use SMTP/TLS (SMTPS: SMTP over direct TLS connection) + # * :open_timeout Number of seconds to wait while attempting to open a connection. + # * :read_timeout Number of seconds to wait until timing-out a read(2) call. # # * sendmail_settings - Allows you to override options for the :sendmail delivery method. # * :location - The location of the sendmail executable. Defaults to /usr/sbin/sendmail. - # * :arguments - The command line arguments. Defaults to -i with -f sender@address + # * :arguments - The command line arguments. Defaults to %w[ -i ] with -f sender@address # added automatically before the message is sent. # # * file_settings - Allows you to override options for the :file delivery method. @@ -433,12 +468,19 @@ module ActionMailer # * deliveries - Keeps an array of all the emails sent out through the Action Mailer with # delivery_method :test. Most useful for unit and functional testing. # - # * deliver_later_queue_name - The name of the queue used with deliver_later. Defaults to +mailers+. + # * delivery_job - The job class used with deliver_later. Mailers can set this to use a + # custom delivery job. Defaults to +ActionMailer::MailDeliveryJob+. + # + # * deliver_later_queue_name - The queue name used by deliver_later with the default + # delivery_job. Mailers can set this to use a custom queue name. class Base < AbstractController::Base + include Callbacks include DeliveryMethods + include QueuedDelivery include Rescuable include Parameterized include Previews + include FormBuilder abstract! @@ -457,7 +499,6 @@ class Base < AbstractController::Base helper ActionMailer::MailHelper - class_attribute :delivery_job, default: ::ActionMailer::DeliveryJob class_attribute :default_params, default: { mime_version: "1.0", charset: "UTF-8", @@ -487,28 +528,28 @@ def unregister_interceptors(*interceptors) end # Register an Observer which will be notified when mail is delivered. - # Either a class, string or symbol can be passed in as the Observer. + # Either a class, string, or symbol can be passed in as the Observer. # If a string or symbol is passed in it will be camelized and constantized. def register_observer(observer) Mail.register_observer(observer_class_for(observer)) end # Unregister a previously registered Observer. - # Either a class, string or symbol can be passed in as the Observer. + # Either a class, string, or symbol can be passed in as the Observer. # If a string or symbol is passed in it will be camelized and constantized. def unregister_observer(observer) Mail.unregister_observer(observer_class_for(observer)) end # Register an Interceptor which will be called before mail is sent. - # Either a class, string or symbol can be passed in as the Interceptor. + # Either a class, string, or symbol can be passed in as the Interceptor. # If a string or symbol is passed in it will be camelized and constantized. def register_interceptor(interceptor) Mail.register_interceptor(observer_class_for(interceptor)) end # Unregister a previously registered Interceptor. - # Either a class, string or symbol can be passed in as the Interceptor. + # Either a class, string, or symbol can be passed in as the Interceptor. # If a string or symbol is passed in it will be camelized and constantized. def unregister_interceptor(interceptor) Mail.unregister_interceptor(observer_class_for(interceptor)) @@ -533,53 +574,22 @@ def mailer_name attr_writer :mailer_name alias :controller_path :mailer_name - # Sets the defaults through app configuration: - # - # config.action_mailer.default(from: "no-reply@example.org") + # Allows to set defaults through app configuration: # - # Aliased by ::default_options= + # config.action_mailer.default_options = { from: "no-reply@example.org" } def default(value = nil) self.default_params = default_params.merge(value).freeze if value default_params end - # Allows to set defaults through app configuration: - # - # config.action_mailer.default_options = { from: "no-reply@example.org" } alias :default_options= :default - # Receives a raw email, parses it into an email object, decodes it, - # instantiates a new mailer, and passes the email object to the mailer - # object's +receive+ method. - # - # If you want your mailer to be able to process incoming messages, you'll - # need to implement a +receive+ method that accepts the raw email string - # as a parameter: + # Wraps an email delivery inside of ActiveSupport::Notifications instrumentation. # - # class MyMailer < ActionMailer::Base - # def receive(mail) - # # ... - # end - # end - def receive(raw_mail) - ActiveSupport::Deprecation.warn(<<~MESSAGE.squish) - ActionMailer::Base.receive is deprecated and will be removed in Rails 6.1. - Use Action Mailbox to process inbound email. - MESSAGE - - ActiveSupport::Notifications.instrument("receive.action_mailer") do |payload| - mail = Mail.new(raw_mail) - set_payload_for_mail(payload, mail) - new.receive(mail) - end - end - - # Wraps an email delivery inside of ActiveSupport::Notifications instrumentation. - # - # This method is actually called by the Mail::Message object itself - # through a callback when you call :deliver on the Mail::Message, - # calling +deliver_mail+ directly and passing a Mail::Message will do + # This method is actually called by the +Mail::Message+ object itself + # through a callback when you call :deliver on the +Mail::Message+, + # calling +deliver_mail+ directly and passing a +Mail::Message+ will do # nothing except tell the logger you sent the email. - def deliver_mail(mail) #:nodoc: + def deliver_mail(mail) # :nodoc: ActiveSupport::Notifications.instrument("deliver.action_mailer") do |payload| set_payload_for_mail(payload, mail) yield # Let Mail do the delivery actions @@ -587,10 +597,12 @@ def deliver_mail(mail) #:nodoc: end # Returns an email in the format "Name ". + # + # If the name is a blank string, it returns just the address. def email_address_with_name(address, name) Mail::Address.new.tap do |builder| builder.address = address - builder.display_name = name + builder.display_name = name.presence end.to_s end @@ -608,17 +620,16 @@ def set_payload_for_mail(payload, mail) payload[:perform_deliveries] = mail.perform_deliveries end - def method_missing(method_name, *args) - if action_methods.include?(method_name.to_s) - MessageDelivery.new(self, method_name, *args) + def method_missing(method_name, ...) + if action_methods.include?(method_name.name) + MessageDelivery.new(self, method_name, ...) else super end end - ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true) def respond_to_missing?(method, include_all = false) - action_methods.include?(method.to_s) || super + action_methods.include?(method.name) || super end end @@ -630,7 +641,7 @@ def initialize @_message = Mail.new end - def process(method_name, *args) #:nodoc: + def process(method_name, *args) # :nodoc: payload = { mailer: self.class.name, action: method_name, @@ -642,8 +653,9 @@ def process(method_name, *args) #:nodoc: @_message = NullMail.new unless @_mail_was_called end end + ruby2_keywords(:process) - class NullMail #:nodoc: + class NullMail # :nodoc: def body; "" end def header; {} end @@ -651,7 +663,7 @@ def respond_to?(string, include_all = false) true end - def method_missing(*args) + def method_missing(...) nil end end @@ -662,22 +674,24 @@ def mailer_name end # Returns an email in the format "Name ". + # + # If the name is a blank string, it returns just the address. def email_address_with_name(address, name) self.class.email_address_with_name(address, name) end - # Allows you to pass random and unusual headers to the new Mail::Message + # Allows you to pass random and unusual headers to the new +Mail::Message+ # object which will add them to itself. # # headers['X-Special-Domain-Specific-Header'] = "SecretValue" # # You can also pass a hash into headers of header field names and values, - # which will then be set on the Mail::Message object: + # which will then be set on the +Mail::Message+ object: # # headers 'X-Special-Domain-Specific-Header' => "SecretValue", # 'In-Reply-To' => incoming.message_id # - # The resulting Mail::Message will have the following in its header: + # The resulting +Mail::Message+ will have the following in its header: # # X-Special-Domain-Specific-Header: SecretValue # @@ -713,7 +727,7 @@ def headers(args = nil) # mail.attachments['filename.jpg'] = File.read('/path/to/filename.jpg') # # If you do this, then Mail will take the file name and work out the mime type. - # It will also set the Content-Type, Content-Disposition, Content-Transfer-Encoding + # It will also set the +Content-Type+, +Content-Disposition+, and +Content-Transfer-Encoding+, # and encode the contents of the attachment in Base64. # # You can also specify overrides if you want by passing a hash instead of a string: @@ -764,7 +778,7 @@ def _raise_error # the most used headers in an email message, these are: # # * +:subject+ - The subject of the message, if this is omitted, Action Mailer will - # ask the Rails I18n class for a translated +:subject+ in the scope of + # ask the \Rails I18n class for a translated +:subject+ in the scope of # [mailer_scope, action_name] or if this is missing, will translate the # humanized version of the +action_name+ # * +:to+ - Who the message is destined for, can be a string of addresses, or an array @@ -774,7 +788,7 @@ def _raise_error # or an array of addresses. # * +:bcc+ - Who you would like to Blind-Carbon-Copy on this email, can be a string of # addresses, or an array of addresses. - # * +:reply_to+ - Who to set the Reply-To header of the email to. + # * +:reply_to+ - Who to set the +Reply-To+ header of the email to. # * +:date+ - The date to say the email was sent on. # # You can set default values for any of the above headers (except +:date+) @@ -801,7 +815,7 @@ def _raise_error # templates in the view paths using by default the mailer name and the # method name that it is being called from, it will then create parts for # each of these templates intelligently, making educated guesses on correct - # content type and sequence, and return a fully prepared Mail::Message + # content type and sequence, and return a fully prepared +Mail::Message+ # ready to call :deliver on to send. # # For example: @@ -868,6 +882,7 @@ def mail(headers = {}, &block) @_mail_was_called = true create_parts_from_responses(message, responses) + wrap_inline_attachments(message) # Set up content type, reapply charset and handle parts order message.content_type = set_content_type(message, content_type, headers[:content_type]) @@ -897,7 +912,7 @@ def set_content_type(m, user_content_type, class_default) # :doc: when user_content_type.present? user_content_type when m.has_attachments? - if m.attachments.detect(&:inline?) + if m.attachments.all?(&:inline?) ["multipart", "related", params] else ["multipart", "mixed", params] @@ -909,13 +924,13 @@ def set_content_type(m, user_content_type, class_default) # :doc: end end - # Translates the +subject+ using Rails I18n class under [mailer_scope, action_name] scope. + # Translates the +subject+ using \Rails I18n class under [mailer_scope, action_name] scope. # If it does not find a translation for the +subject+ under the specified scope it will default to a # humanized version of the action_name. # If the subject has interpolations, you can pass them through the +interpolations+ parameter. def default_i18n_subject(interpolations = {}) # :doc: mailer_scope = self.class.mailer_name.tr("/", ".") - I18n.t(:subject, **interpolations.merge(scope: [mailer_scope, action_name], default: action_name.humanize)) + I18n.t(:subject, **interpolations, scope: [mailer_scope, action_name], default: action_name.humanize) end # Emails do not support relative path links. @@ -924,7 +939,7 @@ def self.supports_path? # :doc: end def apply_defaults(headers) - default_values = self.class.default.transform_values do |value| + default_values = self.class.default.except(*headers.keys).transform_values do |value| compute_default(value) end @@ -995,6 +1010,27 @@ def each_template(paths, name, &block) end end + def wrap_inline_attachments(message) + # If we have both types of attachment, wrap all the inline attachments + # in multipart/related, but not the actual attachments + if message.attachments.detect(&:inline?) && message.attachments.detect { |a| !a.inline? } + related = Mail::Part.new + related.content_type = "multipart/related" + mixed = [ related ] + + message.parts.each do |p| + if p.attachment? && !p.inline? + mixed << p + else + related.add_part(p) + end + end + + message.parts.clear + mixed.each { |c| message.add_part(c) } + end + end + def create_parts_from_responses(m, responses) if responses.size == 1 && !m.has_attachments? responses[0].each { |k, v| m[k] = v } diff --git a/actionmailer/lib/action_mailer/callbacks.rb b/actionmailer/lib/action_mailer/callbacks.rb new file mode 100644 index 0000000000000..fa71f0b4f1b2e --- /dev/null +++ b/actionmailer/lib/action_mailer/callbacks.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module ActionMailer + module Callbacks + extend ActiveSupport::Concern + + included do + include ActiveSupport::Callbacks + define_callbacks :deliver, skip_after_callbacks_if_terminated: true + end + + module ClassMethods + # Defines a callback that will get called right before the + # message is sent to the delivery method. + def before_deliver(*filters, &blk) + set_callback(:deliver, :before, *filters, &blk) + end + + # Defines a callback that will get called right after the + # message's delivery method is finished. + def after_deliver(*filters, &blk) + set_callback(:deliver, :after, *filters, &blk) + end + + # Defines a callback that will get called around the message's deliver method. + def around_deliver(*filters, &blk) + set_callback(:deliver, :around, *filters, &blk) + end + end + end +end diff --git a/actionmailer/lib/action_mailer/delivery_job.rb b/actionmailer/lib/action_mailer/delivery_job.rb deleted file mode 100644 index bcef19080dfcf..0000000000000 --- a/actionmailer/lib/action_mailer/delivery_job.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require "active_job" - -module ActionMailer - # The ActionMailer::DeliveryJob class is used when you - # want to send emails outside of the request-response cycle. - # - # Exceptions are rescued and handled by the mailer class. - class DeliveryJob < ActiveJob::Base # :nodoc: - queue_as { ActionMailer::Base.deliver_later_queue_name } - - rescue_from StandardError, with: :handle_exception_with_mailer_class - - before_perform do - ActiveSupport::Deprecation.warn <<~MSG.squish - Sending mail with DeliveryJob and Parameterized::DeliveryJob - is deprecated and will be removed in Rails 6.1. - Please use MailDeliveryJob instead. - MSG - end - - def perform(mailer, mail_method, delivery_method, *args) #:nodoc: - mailer.constantize.public_send(mail_method, *args).send(delivery_method) - end - ruby2_keywords(:perform) if respond_to?(:ruby2_keywords, true) - - private - # "Deserialize" the mailer class name by hand in case another argument - # (like a Global ID reference) raised DeserializationError. - def mailer_class - if mailer = Array(@serialized_arguments).first || Array(arguments).first - mailer.constantize - end - end - - def handle_exception_with_mailer_class(exception) - if klass = mailer_class - klass.handle_exception exception - else - raise exception - end - end - end -end diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb index 5cd62307e6427..df6e90db7adcc 100644 --- a/actionmailer/lib/action_mailer/delivery_methods.rb +++ b/actionmailer/lib/action_mailer/delivery_methods.rb @@ -3,6 +3,8 @@ require "tmpdir" module ActionMailer + # = Action Mailer \DeliveryMethods + # # This module handles everything related to mail delivery, from registering # new delivery methods to configuring the mail object to be sent. module DeliveryMethods @@ -12,7 +14,6 @@ module DeliveryMethods # Do not make this inheritable, because we always want it to propagate cattr_accessor :raise_delivery_errors, default: true cattr_accessor :perform_deliveries, default: true - cattr_accessor :deliver_later_queue_name, default: :mailers class_attribute :delivery_methods, default: {}.freeze class_attribute :delivery_method, default: :smtp @@ -31,7 +32,7 @@ module DeliveryMethods add_delivery_method :sendmail, Mail::Sendmail, location: "/usr/sbin/sendmail", - arguments: "-i" + arguments: %w[-i] add_delivery_method :test, Mail::TestMailer end @@ -46,10 +47,10 @@ module ClassMethods # # add_delivery_method :sendmail, Mail::Sendmail, # location: '/usr/sbin/sendmail', - # arguments: '-i' + # arguments: %w[ -i ] def add_delivery_method(symbol, klass, default_options = {}) class_attribute(:"#{symbol}_settings") unless respond_to?(:"#{symbol}_settings") - send(:"#{symbol}_settings=", default_options) + public_send(:"#{symbol}_settings=", default_options) self.delivery_methods = delivery_methods.merge(symbol.to_sym => klass).freeze end diff --git a/actionmailer/lib/action_mailer/deprecator.rb b/actionmailer/lib/action_mailer/deprecator.rb new file mode 100644 index 0000000000000..26fde3a857ece --- /dev/null +++ b/actionmailer/lib/action_mailer/deprecator.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ActionMailer + def self.deprecator # :nodoc: + @deprecator ||= ActiveSupport::Deprecation.new + end +end diff --git a/actionmailer/lib/action_mailer/form_builder.rb b/actionmailer/lib/action_mailer/form_builder.rb new file mode 100644 index 0000000000000..048ea337822fc --- /dev/null +++ b/actionmailer/lib/action_mailer/form_builder.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module ActionMailer + # = Action Mailer Form Builder + # + # Override the default form builder for all views rendered by this + # mailer and any of its descendants. Accepts a subclass of + # ActionView::Helpers::FormBuilder. + # + # While emails typically will not include forms, this can be used + # by views that are shared between controllers and mailers. + # + # For more information, see +ActionController::FormBuilder+. + module FormBuilder + extend ActiveSupport::Concern + + included do + class_attribute :_default_form_builder, instance_accessor: false + end + + module ClassMethods + # Set the form builder to be used as the default for all forms + # in the views rendered by this mailer and its subclasses. + # + # ==== Parameters + # * builder - Default form builder. Accepts a subclass of ActionView::Helpers::FormBuilder + def default_form_builder(builder) + self._default_form_builder = builder + end + end + + # Default form builder for the mailer + def default_form_builder + self.class._default_form_builder + end + end +end diff --git a/actionmailer/lib/action_mailer/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb index 78235dee14638..0317b4ebdf600 100644 --- a/actionmailer/lib/action_mailer/gem_version.rb +++ b/actionmailer/lib/action_mailer/gem_version.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true module ActionMailer - # Returns the version of the currently loaded Action Mailer as a Gem::Version. + # Returns the currently loaded version of Action Mailer as a +Gem::Version+. def self.gem_version Gem::Version.new VERSION::STRING end module VERSION - MAJOR = 6 + MAJOR = 8 MINOR = 1 TINY = 0 PRE = "alpha" diff --git a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb index 2b97ac5b9447a..6b47f1b961137 100644 --- a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb +++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb @@ -3,8 +3,10 @@ require "base64" module ActionMailer + # = Action Mailer \InlinePreviewInterceptor + # # Implements a mailer preview interceptor that converts image tag src attributes - # that use inline cid: style URLs to data: style URLs so that they are visible + # that use inline +cid:+ style URLs to +data:+ style URLs so that they are visible # when previewing an HTML email in a web browser. # # This interceptor is enabled by default. To disable it, delete it from the @@ -17,15 +19,15 @@ class InlinePreviewInterceptor include Base64 - def self.previewing_email(message) #:nodoc: + def self.previewing_email(message) # :nodoc: new(message).transform! end - def initialize(message) #:nodoc: + def initialize(message) # :nodoc: @message = message end - def transform! #:nodoc: + def transform! # :nodoc: return message if html_part.blank? html_part.body = html_part.decoded.gsub(PATTERN) do |match| diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb index 26910f20f0fa6..130d5d83e62df 100644 --- a/actionmailer/lib/action_mailer/log_subscriber.rb +++ b/actionmailer/lib/action_mailer/log_subscriber.rb @@ -3,14 +3,17 @@ require "active_support/log_subscriber" module ActionMailer + # = Action Mailer \LogSubscriber + # # Implements the ActiveSupport::LogSubscriber for logging notifications when # email is delivered or received. class LogSubscriber < ActiveSupport::LogSubscriber # An email was delivered. def deliver(event) info do - perform_deliveries = event.payload[:perform_deliveries] - if perform_deliveries + if exception = event.payload[:exception_object] + "Failed delivery of mail #{event.payload[:message_id]} error_class=#{exception.class} error_message=#{exception.message.inspect}" + elsif event.payload[:perform_deliveries] "Delivered mail #{event.payload[:message_id]} (#{event.duration.round(1)}ms)" else "Skipped delivery of mail #{event.payload[:message_id]} as `perform_deliveries` is false" @@ -19,12 +22,7 @@ def deliver(event) debug { event.payload[:mail] } end - - # An email was received. - def receive(event) - info { "Received mail (#{event.duration.round(1)}ms)" } - debug { event.payload[:mail] } - end + subscribe_log_level :deliver, :debug # An email was generated. def process(event) @@ -34,6 +32,7 @@ def process(event) "#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms" end end + subscribe_log_level :process, :debug # Use the logger configured for ActionMailer::Base. def logger diff --git a/actionmailer/lib/action_mailer/mail_delivery_job.rb b/actionmailer/lib/action_mailer/mail_delivery_job.rb index dd6e6234c0175..d76a7cf1155c8 100644 --- a/actionmailer/lib/action_mailer/mail_delivery_job.rb +++ b/actionmailer/lib/action_mailer/mail_delivery_job.rb @@ -3,13 +3,18 @@ require "active_job" module ActionMailer - # The ActionMailer::MailDeliveryJob class is used when you + # = Action Mailer \MailDeliveryJob + # + # The +ActionMailer::MailDeliveryJob+ class is used when you # want to send emails outside of the request-response cycle. It supports # sending either parameterized or normal mail. # # Exceptions are rescued and handled by the mailer class. class MailDeliveryJob < ActiveJob::Base # :nodoc: - queue_as { ActionMailer::Base.deliver_later_queue_name } + queue_as do + mailer_class = arguments.first.constantize + mailer_class.deliver_later_queue_name + end rescue_from StandardError, with: :handle_exception_with_mailer_class diff --git a/actionmailer/lib/action_mailer/mail_helper.rb b/actionmailer/lib/action_mailer/mail_helper.rb index e7bed41f8d294..13c8571dc45f7 100644 --- a/actionmailer/lib/action_mailer/mail_helper.rb +++ b/actionmailer/lib/action_mailer/mail_helper.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module ActionMailer + # = Action Mailer \MailHelper + # # Provides helper methods for ActionMailer::Base that can be used for easily # formatting messages, accessing mailer or message instances, and the # attachments list. @@ -23,10 +25,18 @@ def block_format(text) }.join("\n\n") # Make list points stand on their own line - formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { " #{$1} #{$2.strip}\n" } - formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { " #{$1} #{$2.strip}\n" } + output = +"" + splits = formatted.split(/(\*+|\#+)/) + while line = splits.shift + if line.start_with?("*", "#") && splits.first&.start_with?(" ") + output.chomp!(" ") while output.end_with?(" ") + output << " #{line} #{splits.shift.strip}\n" + else + output << line + end + end - formatted + output end # Access the mailer instance. diff --git a/actionmailer/lib/action_mailer/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb index 74e788876bdfb..504bb25312765 100644 --- a/actionmailer/lib/action_mailer/message_delivery.rb +++ b/actionmailer/lib/action_mailer/message_delivery.rb @@ -3,11 +3,13 @@ require "delegate" module ActionMailer - # The ActionMailer::MessageDelivery class is used by + # = Action Mailer \MessageDelivery + # + # The +ActionMailer::MessageDelivery+ class is used by # ActionMailer::Base when creating a new mailer. # MessageDelivery is a wrapper (+Delegator+ subclass) around a lazy - # created Mail::Message. You can get direct access to the - # Mail::Message, deliver the email or schedule the email to be sent + # created +Mail::Message+. You can get direct access to the + # +Mail::Message+, deliver the email or schedule the email to be sent # through Active Job. # # Notifier.welcome(User.first) # an ActionMailer::MessageDelivery object @@ -15,7 +17,7 @@ module ActionMailer # Notifier.welcome(User.first).deliver_later # enqueue email delivery as a job through Active Job # Notifier.welcome(User.first).message # a Mail::Message object class MessageDelivery < Delegator - def initialize(mailer_class, action, *args) #:nodoc: + def initialize(mailer_class, action, *args) # :nodoc: @mailer_class, @action, @args = mailer_class, action, args # The mail is only processed if we try to call any methods on it. @@ -23,15 +25,15 @@ def initialize(mailer_class, action, *args) #:nodoc: @processed_mailer = nil @mail_message = nil end - ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true) + ruby2_keywords(:initialize) # Method calls are delegated to the Mail::Message that's ready to deliver. - def __getobj__ #:nodoc: + def __getobj__ # :nodoc: @mail_message ||= processed_mailer.message end # Unused except for delegator internals (dup, marshalling). - def __setobj__(mail_message) #:nodoc: + def __setobj__(mail_message) # :nodoc: @mail_message = mail_message end @@ -53,16 +55,19 @@ def processed? # Notifier.welcome(User.first).deliver_later! # Notifier.welcome(User.first).deliver_later!(wait: 1.hour) # Notifier.welcome(User.first).deliver_later!(wait_until: 10.hours.from_now) + # Notifier.welcome(User.first).deliver_later!(priority: 10) # # Options: # # * :wait - Enqueue the email to be delivered with a delay # * :wait_until - Enqueue the email to be delivered at (after) a specific date / time # * :queue - Enqueue the email on the specified queue + # * :priority - Enqueues the email with the specified priority # - # By default, the email will be enqueued using ActionMailer::DeliveryJob. Each - # ActionMailer::Base class can specify the job to use by setting the class variable - # +delivery_job+. + # By default, the email will be enqueued using ActionMailer::MailDeliveryJob on + # the default queue. Mailer classes can customize the queue name used for the default + # job by assigning a +deliver_later_queue_name+ class variable, or provide a custom job + # by assigning a +delivery_job+. When a custom job is used, it controls the queue name. # # class AccountRegistrationMailer < ApplicationMailer # self.delivery_job = RegistrationDeliveryJob @@ -77,16 +82,19 @@ def deliver_later!(options = {}) # Notifier.welcome(User.first).deliver_later # Notifier.welcome(User.first).deliver_later(wait: 1.hour) # Notifier.welcome(User.first).deliver_later(wait_until: 10.hours.from_now) + # Notifier.welcome(User.first).deliver_later(priority: 10) # # Options: # # * :wait - Enqueue the email to be delivered with a delay. # * :wait_until - Enqueue the email to be delivered at (after) a specific date / time. # * :queue - Enqueue the email on the specified queue. + # * :priority - Enqueues the email with the specified priority # - # By default, the email will be enqueued using ActionMailer::DeliveryJob. Each - # ActionMailer::Base class can specify the job to use by setting the class variable - # +delivery_job+. + # By default, the email will be enqueued using ActionMailer::MailDeliveryJob on + # the default queue. Mailer classes can customize the queue name used for the default + # job by assigning a +deliver_later_queue_name+ class variable, or provide a custom job + # by assigning a +delivery_job+. When a custom job is used, it controls the queue name. # # class AccountRegistrationMailer < ApplicationMailer # self.delivery_job = RegistrationDeliveryJob @@ -102,7 +110,9 @@ def deliver_later(options = {}) # def deliver_now! processed_mailer.handle_exceptions do - message.deliver! + processed_mailer.run_callbacks(:deliver) do + message.deliver! + end end end @@ -112,13 +122,15 @@ def deliver_now! # def deliver_now processed_mailer.handle_exceptions do - message.deliver + processed_mailer.run_callbacks(:deliver) do + message.deliver + end end end private # Returns the processed Mailer instance. We keep this instance - # on hand so we can delegate exception handling to it. + # on hand so we can run callbacks and delegate exception handling to it. def processed_mailer @processed_mailer ||= @mailer_class.new.tap do |mailer| mailer.process @action, *@args @@ -136,15 +148,8 @@ def enqueue_delivery(delivery_method, options = {}) "#deliver_later, 2. only touch the message *within your mailer " \ "method*, or 3. use a custom Active Job instead of #deliver_later." else - job = @mailer_class.delivery_job - - if job <= MailDeliveryJob - job.set(options).perform_later( - @mailer_class.name, @action.to_s, delivery_method.to_s, args: @args) - else - job.set(options).perform_later( - @mailer_class.name, @action.to_s, delivery_method.to_s, *@args) - end + @mailer_class.delivery_job.set(options).perform_later( + @mailer_class.name, @action.to_s, delivery_method.to_s, args: @args) end end end diff --git a/actionmailer/lib/action_mailer/parameterized.rb b/actionmailer/lib/action_mailer/parameterized.rb index 65dc5b69cf07f..cbfba5f1deedc 100644 --- a/actionmailer/lib/action_mailer/parameterized.rb +++ b/actionmailer/lib/action_mailer/parameterized.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module ActionMailer + # = Action Mailer \Parameterized + # # Provides the option to parameterize mailers in order to share instance variable # setup, processing, and common headers. # @@ -88,7 +90,11 @@ module Parameterized extend ActiveSupport::Concern included do - attr_accessor :params + attr_writer :params + + def params + @params ||= {} + end end module ClassMethods @@ -108,33 +114,24 @@ def initialize(mailer, params) end private - def method_missing(method_name, *args) - if @mailer.action_methods.include?(method_name.to_s) - ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, @params, *args) + def method_missing(method_name, ...) + if @mailer.action_methods.include?(method_name.name) + ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, @params, ...) else super end end - ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true) def respond_to_missing?(method, include_all = false) @mailer.respond_to?(method, include_all) end end - class DeliveryJob < ActionMailer::DeliveryJob # :nodoc: - def perform(mailer, mail_method, delivery_method, params, *args) - mailer.constantize.with(params).public_send(mail_method, *args).send(delivery_method) - end - ruby2_keywords(:perform) if respond_to?(:ruby2_keywords, true) - end - class MessageDelivery < ActionMailer::MessageDelivery # :nodoc: - def initialize(mailer_class, action, params, *args) - super(mailer_class, action, *args) + def initialize(mailer_class, action, params, ...) + super(mailer_class, action, ...) @params = params end - ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true) private def processed_mailer @@ -148,23 +145,8 @@ def enqueue_delivery(delivery_method, options = {}) if processed? super else - job = delivery_job_class - - if job <= MailDeliveryJob - job.set(options).perform_later( - @mailer_class.name, @action.to_s, delivery_method.to_s, params: @params, args: @args) - else - job.set(options).perform_later( - @mailer_class.name, @action.to_s, delivery_method.to_s, @params, *@args) - end - end - end - - def delivery_job_class - if @mailer_class.delivery_job <= MailDeliveryJob - @mailer_class.delivery_job - else - Parameterized::DeliveryJob + @mailer_class.delivery_job.set(options).perform_later( + @mailer_class.name, @action.to_s, delivery_method.to_s, params: @params, args: @args) end end end diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb index a763b9776cc91..814f256cef1d2 100644 --- a/actionmailer/lib/action_mailer/preview.rb +++ b/actionmailer/lib/action_mailer/preview.rb @@ -3,15 +3,15 @@ require "active_support/descendants_tracker" module ActionMailer - module Previews #:nodoc: + module Previews # :nodoc: extend ActiveSupport::Concern included do - # Set the location of mailer previews through app configuration: + # Add the location of mailer previews through app configuration: # - # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews" + # config.action_mailer.preview_paths << "#{Rails.root}/lib/mailer_previews" # - mattr_accessor :preview_path, instance_writer: false + mattr_accessor :preview_paths, instance_writer: false, default: [] # Enable or disable mailer previews through app configuration: # @@ -79,7 +79,7 @@ class << self # Returns all mailer preview classes. def all load_previews if descendants.empty? - descendants + descendants.sort_by { |mailer| mailer.name.titleize } end # Returns the mail object for the given email name. The registered preview @@ -114,18 +114,18 @@ def find(preview) # Returns the underscored name of the mailer preview without the suffix. def preview_name - name.sub(/Preview$/, "").underscore + name.delete_suffix("Preview").underscore end private def load_previews - if preview_path - Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file } + preview_paths.each do |preview_path| + Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require file } end end - def preview_path - Base.preview_path + def preview_paths + Base.preview_paths end def show_previews diff --git a/actionmailer/lib/action_mailer/queued_delivery.rb b/actionmailer/lib/action_mailer/queued_delivery.rb new file mode 100644 index 0000000000000..1624b62aa373d --- /dev/null +++ b/actionmailer/lib/action_mailer/queued_delivery.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module ActionMailer + module QueuedDelivery + extend ActiveSupport::Concern + + included do + class_attribute :delivery_job, default: ::ActionMailer::MailDeliveryJob + class_attribute :deliver_later_queue_name, default: :mailers + end + end +end diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index 273aabb5f924d..1f1a41e1ce3ed 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -8,8 +8,13 @@ module ActionMailer class Railtie < Rails::Railtie # :nodoc: config.action_mailer = ActiveSupport::OrderedOptions.new + config.action_mailer.preview_paths = [] config.eager_load_namespaces << ActionMailer + initializer "action_mailer.deprecator", before: :load_environment_config do |app| + app.deprecators[:action_mailer] = ActionMailer.deprecator + end + initializer "action_mailer.logger" do ActiveSupport.on_load(:action_mailer) { self.logger ||= Rails.logger } end @@ -23,10 +28,7 @@ class Railtie < Rails::Railtie # :nodoc: options.stylesheets_dir ||= paths["public/stylesheets"].first options.show_previews = Rails.env.development? if options.show_previews.nil? options.cache_store ||= Rails.cache - - if options.show_previews - options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil - end + options.preview_paths |= ["#{Rails.root}/test/mailers/previews"] # make sure readers methods get compiled options.asset_host ||= app.config.asset_host @@ -40,11 +42,23 @@ class Railtie < Rails::Railtie # :nodoc: register_interceptors(options.delete(:interceptors)) register_preview_interceptors(options.delete(:preview_interceptors)) register_observers(options.delete(:observers)) + self.preview_paths |= options[:preview_paths] if delivery_job = options.delete(:delivery_job) self.delivery_job = delivery_job.constantize end + if options.smtp_settings + self.smtp_settings = options.smtp_settings + end + + smtp_timeout = options.delete(:smtp_timeout) + + if self.smtp_settings && smtp_timeout + self.smtp_settings[:open_timeout] ||= smtp_timeout + self.smtp_settings[:read_timeout] ||= smtp_timeout + end + options.each { |k, v| send("#{k}=", v) } end @@ -54,24 +68,9 @@ class Railtie < Rails::Railtie # :nodoc: end end - initializer "action_mailer.set_autoload_paths" do |app| + initializer "action_mailer.set_autoload_paths", before: :set_autoload_paths do |app| options = app.config.action_mailer - - if options.show_previews && options.preview_path - ActiveSupport::Dependencies.autoload_paths << options.preview_path - end - end - - initializer "action_mailer.compile_config_methods" do - ActiveSupport.on_load(:action_mailer) do - config.compile_methods! if config.respond_to?(:compile_methods!) - end - end - - initializer "action_mailer.eager_load_actions" do - ActiveSupport.on_load(:after_initialize) do - ActionMailer::Base.descendants.each(&:action_methods) if config.eager_load - end + app.config.paths["test/mailers/previews"].concat(options.preview_paths) end config.after_initialize do |app| @@ -79,7 +78,8 @@ class Railtie < Rails::Railtie # :nodoc: if options.show_previews app.routes.prepend do - get "/rails/mailers" => "rails/mailers#index", internal: true + get "/rails/mailers" => "rails/mailers#index", internal: true + get "/rails/mailers/download/*path" => "rails/mailers#download", internal: true get "/rails/mailers/*path" => "rails/mailers#preview", internal: true end end diff --git a/actionmailer/lib/action_mailer/rescuable.rb b/actionmailer/lib/action_mailer/rescuable.rb index 5b567eb50005c..2c881505ae27b 100644 --- a/actionmailer/lib/action_mailer/rescuable.rb +++ b/actionmailer/lib/action_mailer/rescuable.rb @@ -1,26 +1,30 @@ # frozen_string_literal: true -module ActionMailer #:nodoc: - # Provides +rescue_from+ for mailers. Wraps mailer action processing, - # mail job processing, and mail delivery. +module ActionMailer # :nodoc: + # = Action Mailer \Rescuable + # + # Provides + # {rescue_from}[rdoc-ref:ActiveSupport::Rescuable::ClassMethods#rescue_from] + # for mailers. Wraps mailer action processing, mail job processing, and mail + # delivery to handle configured errors. module Rescuable extend ActiveSupport::Concern include ActiveSupport::Rescuable class_methods do - def handle_exception(exception) #:nodoc: + def handle_exception(exception) # :nodoc: rescue_with_handler(exception) || raise(exception) end end - def handle_exceptions #:nodoc: + def handle_exceptions # :nodoc: yield rescue => exception rescue_with_handler(exception) || raise end private - def process(*) + def process(...) handle_exceptions do super end diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index 97afd87840e5b..1a07d8e79242e 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -74,6 +74,15 @@ def determine_default_mailer(name) end end + # Reads the fixture file for the given mailer. + # + # This is useful when testing mailers by being able to write the body of + # an email inside a fixture. See the testing guide for a concrete example: + # https://guides.rubyonrails.org/testing.html#revenge-of-the-fixtures + def read_fixture(action) + IO.readlines(File.join(Rails.root, "test", "fixtures", self.class.mailer_class.name.underscore, action)) + end + private def initialize_test_deliveries set_delivery_method :test @@ -110,10 +119,6 @@ def charset def encode(subject) Mail::Encodings.q_value_encode(subject, charset) end - - def read_fixture(action) - IO.readlines(File.join(Rails.root, "test", "fixtures", self.class.mailer_class.name.underscore, action)) - end end include Behavior diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb index 24e6650ff6960..32fcb66873834 100644 --- a/actionmailer/lib/action_mailer/test_helper.rb +++ b/actionmailer/lib/action_mailer/test_helper.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "active_support/core_ext/array/extract_options" require "active_job" module ActionMailer @@ -33,10 +34,8 @@ module TestHelper # end def assert_emails(number, &block) if block_given? - original_count = ActionMailer::Base.deliveries.size - perform_enqueued_jobs(only: ->(job) { delivery_job_filter(job) }, &block) - new_count = ActionMailer::Base.deliveries.size - assert_equal number, new_count - original_count, "#{number} emails expected, but #{new_count - original_count} were sent" + diff = capture_emails(&block).length + assert_equal number, diff, "#{number} emails expected, but #{diff} were sent" else assert_equal number, ActionMailer::Base.deliveries.size end @@ -94,18 +93,50 @@ def assert_enqueued_emails(number, &block) end # Asserts that a specific email has been enqueued, optionally - # matching arguments. + # matching arguments and/or params. # # def test_email # ContactMailer.welcome.deliver_later # assert_enqueued_email_with ContactMailer, :welcome # end # + # def test_email_with_parameters + # ContactMailer.with(greeting: "Hello").welcome.deliver_later + # assert_enqueued_email_with ContactMailer, :welcome, args: { greeting: "Hello" } + # end + # # def test_email_with_arguments # ContactMailer.welcome("Hello", "Goodbye").deliver_later # assert_enqueued_email_with ContactMailer, :welcome, args: ["Hello", "Goodbye"] # end # + # def test_email_with_named_arguments + # ContactMailer.welcome(greeting: "Hello", farewell: "Goodbye").deliver_later + # assert_enqueued_email_with ContactMailer, :welcome, args: [{ greeting: "Hello", farewell: "Goodbye" }] + # end + # + # def test_email_with_parameters_and_arguments + # ContactMailer.with(greeting: "Hello").welcome("Cheers", "Goodbye").deliver_later + # assert_enqueued_email_with ContactMailer, :welcome, params: { greeting: "Hello" }, args: ["Cheers", "Goodbye"] + # end + # + # def test_email_with_parameters_and_named_arguments + # ContactMailer.with(greeting: "Hello").welcome(farewell: "Goodbye").deliver_later + # assert_enqueued_email_with ContactMailer, :welcome, params: { greeting: "Hello" }, args: [{farewell: "Goodbye"}] + # end + # + # def test_email_with_parameterized_mailer + # ContactMailer.with(greeting: "Hello").welcome.deliver_later + # assert_enqueued_email_with ContactMailer.with(greeting: "Hello"), :welcome + # end + # + # def test_email_with_matchers + # ContactMailer.with(greeting: "Hello").welcome("Cheers", "Goodbye").deliver_later + # assert_enqueued_email_with ContactMailer, :welcome, + # params: ->(params) { /hello/i.match?(params[:greeting]) }, + # args: ->(args) { /cheers/i.match?(args[0]) } + # end + # # If a block is passed, that block should cause the specified email # to be enqueued. # @@ -123,13 +154,23 @@ def assert_enqueued_emails(number, &block) # ContactMailer.with(email: 'user@example.com').welcome.deliver_later # end # end - def assert_enqueued_email_with(mailer, method, args: nil, queue: "mailers", &block) - args = if args.is_a?(Hash) - [mailer.to_s, method.to_s, "deliver_now", params: args, args: []] - else - [mailer.to_s, method.to_s, "deliver_now", args: Array(args)] + def assert_enqueued_email_with(mailer, method, params: nil, args: nil, queue: nil, &block) + if mailer.is_a? ActionMailer::Parameterized::Mailer + params = mailer.instance_variable_get(:@params) + mailer = mailer.instance_variable_get(:@mailer) end - assert_enqueued_with(job: mailer.delivery_job, args: args, queue: queue, &block) + + args = Array(args) unless args.is_a?(Proc) + queue ||= mailer.deliver_later_queue_name || ActiveJob::Base.default_queue_name + + expected = ->(job_args) do + job_kwargs = job_args.extract_options! + + [mailer.to_s, method.to_s, "deliver_now"] == job_args && + params === job_kwargs[:params] && args === job_kwargs[:args] + end + + assert_enqueued_with(job: mailer.delivery_job, args: expected, queue: queue.to_s, &block) end # Asserts that no emails are enqueued for later delivery. @@ -151,12 +192,73 @@ def assert_no_enqueued_emails(&block) assert_enqueued_emails 0, &block end + # Delivers all enqueued emails. If a block is given, delivers all of the emails + # that were enqueued throughout the duration of the block. If a block is + # not given, delivers all the enqueued emails up to this point in the test. + # + # def test_deliver_enqueued_emails + # deliver_enqueued_emails do + # ContactMailer.welcome.deliver_later + # end + # + # assert_emails 1 + # end + # + # def test_deliver_enqueued_emails_without_block + # ContactMailer.welcome.deliver_later + # + # deliver_enqueued_emails + # + # assert_emails 1 + # end + # + # If the +:queue+ option is specified, + # then only the emails(s) enqueued to a specific queue will be performed. + # + # def test_deliver_enqueued_emails_with_queue + # deliver_enqueued_emails queue: :external_mailers do + # CustomerMailer.deliver_later_queue_name = :external_mailers + # CustomerMailer.welcome.deliver_later # will be performed + # EmployeeMailer.deliver_later_queue_name = :internal_mailers + # EmployeeMailer.welcome.deliver_later # will not be performed + # end + # + # assert_emails 1 + # end + # + # If the +:at+ option is specified, then only delivers emails enqueued to deliver + # immediately or before the given time. + def deliver_enqueued_emails(queue: nil, at: nil, &block) + perform_enqueued_jobs(only: ->(job) { delivery_job_filter(job) }, queue: queue, at: at, &block) + end + + # Returns any emails that are sent in the block. + # + # def test_emails + # emails = capture_emails do + # ContactMailer.welcome.deliver_now + # end + # assert_equal "Hi there", emails.first.subject + # + # emails = capture_emails do + # ContactMailer.welcome.deliver_now + # ContactMailer.welcome.deliver_later + # end + # assert_equal "Hi there", emails.first.subject + # end + def capture_emails(&block) + original_count = ActionMailer::Base.deliveries.size + deliver_enqueued_emails(&block) + new_count = ActionMailer::Base.deliveries.size + diff = new_count - original_count + ActionMailer::Base.deliveries.last(diff) + end + private def delivery_job_filter(job) job_class = job.is_a?(Hash) ? job.fetch(:job) : job.class - Base.descendants.map(&:delivery_job).include?(job_class) || - ActionMailer::Parameterized::DeliveryJob == job_class + Base.descendants.map(&:delivery_job).include?(job_class) end end end diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb index 4549d6eb57b60..9be6e66e07bb0 100644 --- a/actionmailer/lib/action_mailer/version.rb +++ b/actionmailer/lib/action_mailer/version.rb @@ -3,8 +3,8 @@ require_relative "gem_version" module ActionMailer - # Returns the version of the currently loaded Action Mailer as a - # Gem::Version. + # Returns the currently loaded version of Action Mailer as a + # +Gem::Version+. def self.version gem_version end diff --git a/actionmailer/lib/rails/generators/mailer/USAGE b/actionmailer/lib/rails/generators/mailer/USAGE index 0ea95f2c3f875..6768e5c2a2ac2 100644 --- a/actionmailer/lib/rails/generators/mailer/USAGE +++ b/actionmailer/lib/rails/generators/mailer/USAGE @@ -1,16 +1,20 @@ Description: -============ - Stubs out a new mailer and its views. Passes the mailer name, either + Generates a new mailer and its views. Passes the mailer name, either CamelCased or under_scored, and an optional list of emails as arguments. This generates a mailer class in app/mailers and invokes your template engine and test framework generators. -Example: -======== - bin/rails generate mailer Notifications signup forgot_password invoice +Examples: + `bin/rails generate mailer sign_up` + + creates a sign up mailer class, views, and test: + Mailer: app/mailers/sign_up_mailer.rb + Views: app/views/sign_up_mailer/signup.text.erb [...] + Test: test/mailers/sign_up_mailer_test.rb + + `bin/rails generate mailer notifications sign_up forgot_password invoice` + + creates a notifications mailer with sign_up, forgot_password, and invoice actions. + - creates a Notifications mailer class, views, and test: - Mailer: app/mailers/notifications_mailer.rb - Views: app/views/notifications_mailer/signup.text.erb [...] - Test: test/mailers/notifications_mailer_test.rb diff --git a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt index 348d3147586be..333a9f59b8144 100644 --- a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt +++ b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt @@ -1,7 +1,9 @@ <% module_namespacing do -%> class <%= class_name %>Mailer < ApplicationMailer -<% actions.each do |action| -%> +<% actions.each_with_index do |action, index| -%> +<% if index != 0 -%> +<% end -%> # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index a2a603834cb66..d795e466c3003 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative "../../tools/strict_warnings" require "active_support/core_ext/kernel/reporting" # These are the normal settings that will be set up by Railties @@ -25,7 +26,7 @@ def self.root ActionMailer::Base.include(ActionView::Layouts) # Show backtraces for deprecated behavior for quicker cleanup. -ActiveSupport::Deprecation.debug = true +ActionMailer.deprecator.debug = true # Disable available locale checks to avoid warnings running the test suite. I18n.enforce_available_locales = false @@ -37,17 +38,6 @@ def self.root class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions - - private - # Skips the current run on Rubinius using Minitest::Assertions#skip - def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" - end - - # Skips the current run on JRuby using Minitest::Assertions#skip - def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) - end end require_relative "../../tools/test_common" diff --git a/actionmailer/test/asset_host_test.rb b/actionmailer/test/asset_host_test.rb index 9cd8cae88cd6f..7e0d7e7cc599b 100644 --- a/actionmailer/test/asset_host_test.rb +++ b/actionmailer/test/asset_host_test.rb @@ -29,7 +29,7 @@ def test_asset_host_as_string def test_asset_host_as_one_argument_proc AssetHostMailer.config.asset_host = Proc.new { |source| - if source.starts_with?("/images") + if source.start_with?("/images") "http://images.example.com" end } diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index c980c2d9f6611..49e0cba9fb61a 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "abstract_unit" -require "set" require "action_dispatch" require "active_support/time" @@ -70,6 +69,13 @@ class BaseTest < ActiveSupport::TestCase assert_equal("Welcome", email.body.encoded) end + test "mail() doesn't set the mailer as a controller in the execution context" do + ActiveSupport::ExecutionContext.clear + assert_nil ActiveSupport::ExecutionContext.to_h[:controller] + BaseMailer.welcome(from: "someone@example.com", to: "another@example.org").to + assert_nil ActiveSupport::ExecutionContext.to_h[:controller] + end + test "can pass in :body to the mail method hash" do email = BaseMailer.welcome(body: "Hello there") assert_equal("text/plain", email.mime_type) @@ -89,6 +95,11 @@ class BaseTest < ActiveSupport::TestCase assert_equal("Mikel ", email["Reply-To"].value) end + test "mail() using email_address_with_name with blank string as name" do + email = BaseMailer.with_blank_name + assert_equal("sunny@example.com", email["To"].value) + end + # Custom headers test "custom headers" do email = BaseMailer.welcome @@ -186,7 +197,21 @@ class BaseTest < ActiveSupport::TestCase assert_equal("logo.png", email.parts[1].filename) end - # Defaults values + test "can embed an inline attachment and other attachments" do + email = BaseMailer.inline_and_other_attachments + # Need to call #encoded to force the JIT sort on parts + email.encoded + assert_equal(2, email.parts.length) + assert_equal("multipart/mixed", email.mime_type) + assert_equal("multipart/related", email.parts[0].mime_type) + assert_equal("multipart/alternative", email.parts[0].parts[0].mime_type) + assert_equal("text/plain", email.parts[0].parts[0].parts[0].mime_type) + assert_equal("text/html", email.parts[0].parts[0].parts[1].mime_type) + assert_equal("logo.png", email.parts[0].parts[1].filename) + assert_equal("certificate.pdf", email.parts[1].filename) + end + + # Default values test "uses default charset from class" do with_default BaseMailer, charset: "US-ASCII" do email = BaseMailer.welcome @@ -306,7 +331,7 @@ def welcome mail body: "yay", from: "welcome@example.com", to: "to@example.com" unless attachments.map(&:filename) == ["invoice.pdf"] - raise Minitest::Assertion, "Should allow access to attachments" + flunk("Should allow access to attachments") end end end @@ -806,8 +831,8 @@ def self.previewing_email(mail); end end test "proc default values can have arity of 1 where arg is a mailer instance" do - assert_equal(ProcMailer.welcome["X-Lambda-Arity-1-arg"].to_s, "complex_value") - assert_equal(ProcMailer.welcome["X-Lambda-Arity-1-self"].to_s, "complex_value") + assert_equal("complex_value", ProcMailer.welcome["X-Lambda-Arity-1-arg"].to_s) + assert_equal("complex_value", ProcMailer.welcome["X-Lambda-Arity-1-self"].to_s) end test "proc default values with fixed arity of 0 can be called" do @@ -819,6 +844,14 @@ def self.previewing_email(mail); end assert_equal("Thanks for signing up this afternoon", mail.subject) end + test "proc default values are not evaluated when overridden" do + with_default BaseMailer, from: -> { flunk }, to: -> { flunk } do + email = BaseMailer.welcome(from: "overridden-from@example.com", to: "overridden-to@example.com") + assert_equal ["overridden-from@example.com"], email.from + assert_equal ["overridden-to@example.com"], email.to + end + end + test "modifying the mail message with a before_action" do class BeforeActionMailer < ActionMailer::Base before_action :add_special_header! @@ -930,35 +963,23 @@ def a_callback end test "notification for process" do - events = [] - ActiveSupport::Notifications.subscribe("process.action_mailer") do |*args| - events << ActiveSupport::Notifications::Event.new(*args) - end - - BaseMailer.welcome(body: "Hello there").deliver_now + expected_payload = { mailer: "BaseMailer", action: :welcome, args: [{ body: "Hello there" }] } - assert_equal 1, events.length - assert_equal "process.action_mailer", events[0].name - assert_equal "BaseMailer", events[0].payload[:mailer] - assert_equal :welcome, events[0].payload[:action] - assert_equal [{ body: "Hello there" }], events[0].payload[:args] - ensure - ActiveSupport::Notifications.unsubscribe "process.action_mailer" + assert_notifications_count("process.action_mailer", 1) do + assert_notification("process.action_mailer", expected_payload) do + BaseMailer.welcome(body: "Hello there").deliver_now + end + end end test "notification for deliver" do - events = [] - ActiveSupport::Notifications.subscribe("deliver.action_mailer") do |*args| - events << ActiveSupport::Notifications::Event.new(*args) - end - - BaseMailer.welcome(body: "Hello there").deliver_now + assert_notifications_count("deliver.action_mailer", 1) do + notification = assert_notification("deliver.action_mailer") do + BaseMailer.welcome(body: "Hello there").deliver_now + end - assert_equal 1, events.length - assert_equal "deliver.action_mailer", events[0].name - assert_not_nil events[0].payload[:message_id] - ensure - ActiveSupport::Notifications.unsubscribe "deliver.action_mailer" + assert_not_nil notification.payload[:message_id] + end end private @@ -967,13 +988,13 @@ def a_callback def swap(klass, new_values) old_values = {} new_values.each do |key, value| - old_values[key] = klass.send key - klass.send :"#{key}=", value + old_values[key] = klass.public_send key + klass.public_send :"#{key}=", value end yield ensure old_values.each do |key, value| - klass.send :"#{key}=", value + klass.public_send :"#{key}=", value end end @@ -1097,6 +1118,24 @@ def self.previewing_email(mail); end end end +class PreviewTest < ActiveSupport::TestCase + class A < ActionMailer::Preview; end + + module B + class A < ActionMailer::Preview; end + class C < ActionMailer::Preview; end + end + + class C < ActionMailer::Preview; end + + test "all() returns mailers in alphabetical order" do + ActionMailer::Preview.stub(:descendants, [C, A, B::C, B::A]) do + mailers = ActionMailer::Preview.all + assert_equal [A, B::A, B::C, C], mailers + end + end +end + class BasePreviewTest < ActiveSupport::TestCase class BaseMailerPreview < ActionMailer::Preview def welcome diff --git a/actionmailer/test/caching_test.rb b/actionmailer/test/caching_test.rb index c3a3e418d097d..1a495159b2c45 100644 --- a/actionmailer/test/caching_test.rb +++ b/actionmailer/test/caching_test.rb @@ -171,19 +171,15 @@ def test_multipart_fragment_caching def test_fragment_cache_instrumentation @mailer.enable_fragment_cache_logging = true - payload = nil - subscriber = proc do |*args| - event = ActiveSupport::Notifications::Event.new(*args) - payload = event.payload - end + expected_payload = { + mailer: "caching_mailer", + key: [:views, "caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache", "html")}", :caching] + } - ActiveSupport::Notifications.subscribed(subscriber, "read_fragment.action_mailer") do + assert_notification("read_fragment.action_mailer", expected_payload) do @mailer.fragment_cache end - - assert_equal "caching_mailer", payload[:mailer] - assert_equal [ :views, "caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache", "html")}", :caching ], payload[:key] ensure @mailer.enable_fragment_cache_logging = true end @@ -221,31 +217,8 @@ def self.output_buffer=; end cache_helper.stub :controller, controller do cache_helper.stub :output_buffer, output_buffer do - assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do - assert_nothing_raised do - cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil } - end - end - end - end - end - - def test_safe_buffer - output_buffer = ActiveSupport::SafeBuffer.new - controller = MockController.new - cache_helper = Class.new do - def self.controller; end - def self.output_buffer; end - def self.output_buffer=; end - end - cache_helper.extend(ActionView::Helpers::CacheHelper) - - cache_helper.stub :controller, controller do - cache_helper.stub :output_buffer, output_buffer do - assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do - assert_nothing_raised do - cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil } - end + assert_nothing_raised do + cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil } end end end diff --git a/actionmailer/test/callbacks_test.rb b/actionmailer/test/callbacks_test.rb new file mode 100644 index 0000000000000..3a2cf8f31822c --- /dev/null +++ b/actionmailer/test/callbacks_test.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "mailers/callback_mailer" +require "active_support/testing/stream" + +class ActionMailerCallbacksTest < ActiveSupport::TestCase + include ActiveJob::TestHelper + include ActiveSupport::Testing::Stream + + setup do + @previous_delivery_method = ActionMailer::Base.delivery_method + ActionMailer::Base.delivery_method = :test + CallbackMailer.rescue_from_error = nil + CallbackMailer.after_deliver_instance = nil + CallbackMailer.around_deliver_instance = nil + CallbackMailer.abort_before_deliver = nil + CallbackMailer.around_handles_error = nil + end + + teardown do + ActionMailer::Base.deliveries.clear + ActionMailer::Base.delivery_method = @previous_delivery_method + CallbackMailer.rescue_from_error = nil + CallbackMailer.after_deliver_instance = nil + CallbackMailer.around_deliver_instance = nil + CallbackMailer.abort_before_deliver = nil + CallbackMailer.around_handles_error = nil + end + + test "deliver_now should call after_deliver callback and can access sent message" do + mail_delivery = CallbackMailer.test_message + mail_delivery.deliver_now + + assert_kind_of CallbackMailer, CallbackMailer.after_deliver_instance + assert_not_empty CallbackMailer.after_deliver_instance.message.message_id + assert_equal mail_delivery.message_id, CallbackMailer.after_deliver_instance.message.message_id + assert_equal "test-receiver@test.com", CallbackMailer.after_deliver_instance.message.to.first + end + + test "deliver_now! should call after_deliver callback" do + CallbackMailer.test_message.deliver_now! + + assert_kind_of CallbackMailer, CallbackMailer.after_deliver_instance + end + + test "before_deliver can abort the delivery and not run after_deliver callbacks" do + CallbackMailer.abort_before_deliver = true + + mail_delivery = CallbackMailer.test_message + mail_delivery.deliver_now + + assert_nil mail_delivery.message_id + assert_nil CallbackMailer.after_deliver_instance + end + + test "deliver_later should call after_deliver callback and can access sent message" do + perform_enqueued_jobs do + silence_stream($stdout) do + CallbackMailer.test_message.deliver_later + end + end + assert_kind_of CallbackMailer, CallbackMailer.after_deliver_instance + assert_not_empty CallbackMailer.after_deliver_instance.message.message_id + end + + test "around_deliver is called after rescue_from on action processing exceptions" do + CallbackMailer.around_handles_error = true + + CallbackMailer.test_raise_action.deliver_now + assert CallbackMailer.rescue_from_error + end + + test "around_deliver is called before rescue_from on deliver! exceptions" do + CallbackMailer.around_handles_error = true + + stub_any_instance(Mail::TestMailer, instance: Mail::TestMailer.new({})) do |instance| + instance.stub(:deliver!, proc { raise "boom deliver exception" }) do + CallbackMailer.test_message.deliver_now + end + end + + assert_kind_of CallbackMailer, CallbackMailer.after_deliver_instance + assert_nil CallbackMailer.rescue_from_error + end +end diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb index 025f7152bb792..a39eb37eaa907 100644 --- a/actionmailer/test/delivery_methods_test.rb +++ b/actionmailer/test/delivery_methods_test.rb @@ -41,7 +41,7 @@ class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase test "default sendmail settings" do settings = { location: "/usr/sbin/sendmail", - arguments: "-i" + arguments: %w[ -i ] } assert_equal settings, ActionMailer::Base.sendmail_settings end diff --git a/actionmailer/test/fixtures/attachments/foo.jpg b/actionmailer/test/fixtures/attachments/foo.jpg deleted file mode 100644 index b976fe5e002bf..0000000000000 Binary files a/actionmailer/test/fixtures/attachments/foo.jpg and /dev/null differ diff --git a/actionmailer/test/fixtures/attachments/test.jpg b/actionmailer/test/fixtures/attachments/test.jpg deleted file mode 100644 index b976fe5e002bf..0000000000000 Binary files a/actionmailer/test/fixtures/attachments/test.jpg and /dev/null differ diff --git a/actionmailer/test/fixtures/base_mailer/inline_and_other_attachments.html.erb b/actionmailer/test/fixtures/base_mailer/inline_and_other_attachments.html.erb new file mode 100644 index 0000000000000..e20087812748f --- /dev/null +++ b/actionmailer/test/fixtures/base_mailer/inline_and_other_attachments.html.erb @@ -0,0 +1,5 @@ +

Inline Image

+ +<%= image_tag attachments['logo.png'].url %> + +

This is an image that is inline

\ No newline at end of file diff --git a/actionmailer/test/fixtures/base_mailer/inline_and_other_attachments.text.erb b/actionmailer/test/fixtures/base_mailer/inline_and_other_attachments.text.erb new file mode 100644 index 0000000000000..e161d244d27eb --- /dev/null +++ b/actionmailer/test/fixtures/base_mailer/inline_and_other_attachments.text.erb @@ -0,0 +1,4 @@ +Inline Image + +No image for you + diff --git a/actionmailer/test/fixtures/form_builder_mailer/welcome.html.erb b/actionmailer/test/fixtures/form_builder_mailer/welcome.html.erb new file mode 100644 index 0000000000000..180a827089c00 --- /dev/null +++ b/actionmailer/test/fixtures/form_builder_mailer/welcome.html.erb @@ -0,0 +1,3 @@ +<%= form_with(url: "/") do |f| %> + <%= f.message %> +<% end %> diff --git a/actionmailer/test/fixtures/raw_email b/actionmailer/test/fixtures/raw_email deleted file mode 100644 index 43f7a59cee03f..0000000000000 --- a/actionmailer/test/fixtures/raw_email +++ /dev/null @@ -1,14 +0,0 @@ -From jamis_buck@byu.edu Mon May 2 16:07:05 2005 -Mime-Version: 1.0 (Apple Message framework v622) -Content-Transfer-Encoding: base64 -Message-Id: -Content-Type: text/plain; - charset=EUC-KR; - format=flowed -To: willard15georgina@jamis.backpackit.com -From: Jamis Buck -Subject: =?EUC-KR?Q?NOTE:_=C7=D1=B1=B9=B8=BB=B7=CE_=C7=CF=B4=C2_=B0=CD?= -Date: Mon, 2 May 2005 16:07:05 -0600 - -tOu6zrrQwMcguLbC+bChwfa3ziwgv+y4rrTCIMfPs6q01MC7ILnPvcC0z7TZLg0KDQrBpiDAzLin -wLogSmFtaXPA1LTPtNku diff --git a/actionmailer/test/form_builder_test.rb b/actionmailer/test/form_builder_test.rb new file mode 100644 index 0000000000000..460acd04be822 --- /dev/null +++ b/actionmailer/test/form_builder_test.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "mailers/form_builder_mailer" + +class MailerFormBuilderTest < ActiveSupport::TestCase + def test_default_form_builder_assigned + email = FormBuilderMailer.welcome + assert_includes(email.body.encoded, "hi from SpecializedFormBuilder") + end +end diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb index eea72e06ba612..5e5be4ff2b2a3 100644 --- a/actionmailer/test/i18n_with_controller_test.rb +++ b/actionmailer/test/i18n_with_controller_test.rb @@ -27,7 +27,7 @@ def send_mail class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest Routes = ActionDispatch::Routing::RouteSet.new Routes.draw do - ActiveSupport::Deprecation.silence do + ActionDispatch.deprecator.silence do get ":controller(/:action(/:id))" end end diff --git a/actionmailer/test/legacy_delivery_job_test.rb b/actionmailer/test/legacy_delivery_job_test.rb deleted file mode 100644 index a9b2a160d49fb..0000000000000 --- a/actionmailer/test/legacy_delivery_job_test.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -require "abstract_unit" -require "active_job" -require "mailers/params_mailer" -require "mailers/delayed_mailer" - -class LegacyDeliveryJobTest < ActiveSupport::TestCase - include ActiveJob::TestHelper - - class LegacyDeliveryJob < ActionMailer::DeliveryJob - end - - setup do - @previous_logger = ActiveJob::Base.logger - ActiveJob::Base.logger = Logger.new(nil) - - @previous_delivery_method = ActionMailer::Base.delivery_method - ActionMailer::Base.delivery_method = :test - - @previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name - ActionMailer::Base.deliver_later_queue_name = :test_queue - end - - teardown do - ActiveJob::Base.logger = @previous_logger - ParamsMailer.deliveries.clear - - ActionMailer::Base.delivery_method = @previous_delivery_method - ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name - end - - test "should send parameterized mail correctly" do - mail = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com").invitation - args = [ - "ParamsMailer", - "invitation", - "deliver_now", - { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" }, - ] - - with_delivery_job(LegacyDeliveryJob) do - assert_deprecated do - assert_performed_with(job: ActionMailer::Parameterized::DeliveryJob, args: args) do - mail.deliver_later - end - end - end - end - - test "should send mail correctly" do - mail = DelayedMailer.test_message(1, 2, 3) - args = [ - "DelayedMailer", - "test_message", - "deliver_now", - 1, - 2, - 3, - ] - - with_delivery_job(LegacyDeliveryJob) do - assert_deprecated do - assert_performed_with(job: LegacyDeliveryJob, args: args) do - mail.deliver_later - end - end - end - end - - private - def with_delivery_job(job) - old_params_delivery_job = ParamsMailer.delivery_job - old_regular_delivery_job = DelayedMailer.delivery_job - ParamsMailer.delivery_job = job - DelayedMailer.delivery_job = job - yield - ensure - ParamsMailer.delivery_job = old_params_delivery_job - DelayedMailer.delivery_job = old_regular_delivery_job - end -end diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb index f09f1997c03d0..4272430a9bfd2 100644 --- a/actionmailer/test/log_subscriber_test.rb +++ b/actionmailer/test/log_subscriber_test.rb @@ -13,9 +13,12 @@ def setup ActionMailer::LogSubscriber.attach_to :action_mailer end - class TestMailer < ActionMailer::Base - def receive(mail) - # Do nothing + class BogusDelivery + def initialize(*) + end + + def deliver!(mail) + raise "failed" end end @@ -51,15 +54,16 @@ def test_deliver_message_when_perform_deliveries_is_false BaseMailer.deliveries.clear end - def test_receive_is_notified - fixture = File.read(File.expand_path("fixtures/raw_email", __dir__)) - assert_deprecated do - TestMailer.receive(fixture) - end + def test_deliver_message_when_exception_happened + previous_delivery_method = BaseMailer.delivery_method + BaseMailer.delivery_method = BogusDelivery + + assert_raises(RuntimeError) { BaseMailer.welcome(message_id: "123@abc").deliver_now } wait + assert_equal(1, @logger.logged(:info).size) - assert_match(/Received mail/, @logger.logged(:info).first) - assert_equal(1, @logger.logged(:debug).size) - assert_match(/Jamis/, @logger.logged(:debug).first) + assert_equal('Failed delivery of mail 123@abc error_class=RuntimeError error_message="failed"', @logger.logged(:info).first) + ensure + BaseMailer.delivery_method = previous_delivery_method end end diff --git a/actionmailer/test/mail_helper_test.rb b/actionmailer/test/mail_helper_test.rb index a8ab19a95c0e3..06729826f8480 100644 --- a/actionmailer/test/mail_helper_test.rb +++ b/actionmailer/test/mail_helper_test.rb @@ -121,4 +121,17 @@ def test_use_cache assert_equal "Greetings from a cache helper block", mail.body.encoded end end + + def helper + Object.new.extend(ActionMailer::MailHelper) + end + + def test_block_format + assert_equal " * foo\n", helper.block_format(" * foo") + assert_equal " * foo\n", helper.block_format(" * foo") + assert_equal " * foo\n", helper.block_format("* foo") + assert_equal " * foo\n*bar", helper.block_format("* foo*bar") + assert_equal " * foo\n * bar\n", helper.block_format("* foo * bar") + assert_equal " *", helper.block_format("* ") + end end diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb index 9f3fef23832f4..4a73d40bafb87 100644 --- a/actionmailer/test/mailers/base_mailer.rb +++ b/actionmailer/test/mailers/base_mailer.rb @@ -31,6 +31,11 @@ def with_name mail(template_name: "welcome", to: to) end + def with_blank_name + to = email_address_with_name("sunny@example.com", "") + mail(template_name: "welcome", to: to) + end + def html_only(hash = {}) mail(hash) end @@ -44,6 +49,12 @@ def inline_attachment mail end + def inline_and_other_attachments + attachments.inline["logo.png"] = "\312\213\254\232" + attachments["certificate.pdf"] = "This is test File content" + mail + end + def attachment_with_content(hash = {}) attachments["invoice.pdf"] = "This is test File content" mail(hash) diff --git a/actionmailer/test/mailers/callback_mailer.rb b/actionmailer/test/mailers/callback_mailer.rb new file mode 100644 index 0000000000000..86e6c005d4f56 --- /dev/null +++ b/actionmailer/test/mailers/callback_mailer.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +CallbackMailerError = Class.new(StandardError) +class CallbackMailer < ActionMailer::Base + cattr_accessor :rescue_from_error + cattr_accessor :after_deliver_instance + cattr_accessor :around_deliver_instance + cattr_accessor :abort_before_deliver + cattr_accessor :around_handles_error + + rescue_from CallbackMailerError do |error| + @@rescue_from_error = error + end + + before_deliver do + throw :abort if @@abort_before_deliver + end + + after_deliver do + @@after_deliver_instance = self + end + + around_deliver do |mailer, block| + @@around_deliver_instance = self + block.call + rescue StandardError + raise unless @@around_handles_error + end + + def test_message(*) + mail(from: "test-sender@test.com", to: "test-receiver@test.com", subject: "Test Subject", body: "Test Body") + end + + def test_raise_action + raise CallbackMailerError, "boom action processing" + end +end diff --git a/actionmailer/test/mailers/delayed_mailer.rb b/actionmailer/test/mailers/delayed_mailer.rb index bd335ff56cce5..37c7590d97bb2 100644 --- a/actionmailer/test/mailers/delayed_mailer.rb +++ b/actionmailer/test/mailers/delayed_mailer.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "active_job/arguments" - class DelayedMailerError < StandardError; end class DelayedMailer < ActionMailer::Base + self.deliver_later_queue_name = :delayed_mailers + cattr_accessor :last_error cattr_accessor :last_rescue_from_instance diff --git a/actionmailer/test/mailers/form_builder_mailer.rb b/actionmailer/test/mailers/form_builder_mailer.rb new file mode 100644 index 0000000000000..8e2f00a01b20f --- /dev/null +++ b/actionmailer/test/mailers/form_builder_mailer.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class FormBuilderMailer < ActionMailer::Base + class SpecializedFormBuilder < ActionView::Helpers::FormBuilder + def message + "hi from SpecializedFormBuilder" + end + end + + default_form_builder SpecializedFormBuilder + + def welcome + mail(to: "email@example.com") + end +end diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb index 09eb0c70f652f..81061ff25b5d7 100644 --- a/actionmailer/test/message_delivery_test.rb +++ b/actionmailer/test/message_delivery_test.rb @@ -10,10 +10,10 @@ class MessageDeliveryTest < ActiveSupport::TestCase setup do @previous_logger = ActiveJob::Base.logger @previous_delivery_method = ActionMailer::Base.delivery_method - @previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name - ActionMailer::Base.deliver_later_queue_name = :test_queue - ActionMailer::Base.delivery_method = :test + ActiveJob::Base.logger = Logger.new(nil) + ActionMailer::Base.delivery_method = :test + ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true @@ -28,7 +28,6 @@ class MessageDeliveryTest < ActiveSupport::TestCase ActiveJob::Base.logger = @previous_logger ActionMailer::Base.delivery_method = @previous_delivery_method - ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name DelayedMailer.last_error = nil DelayedMailer.last_rescue_from_instance = nil @@ -58,11 +57,6 @@ class MessageDeliveryTest < ActiveSupport::TestCase assert_respond_to @mail, :deliver_now! end - def test_should_enqueue_and_run_correctly_in_activejob - @mail.deliver_later! - assert_equal 1, ActionMailer::Base.deliveries.size - end - test "should enqueue the email with :deliver_now delivery method" do assert_performed_with(job: ActionMailer::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]]) do @mail.deliver_later @@ -75,7 +69,7 @@ def test_should_enqueue_and_run_correctly_in_activejob end end - test "should enqueue a delivery with a delay" do + test "should enqueue delivery with a delay" do travel_to Time.new(2004, 11, 24, 1, 4, 44) do assert_performed_with(job: ActionMailer::MailDeliveryJob, at: Time.current + 10.minutes, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]]) do @mail.deliver_later wait: 10.minutes @@ -83,20 +77,25 @@ def test_should_enqueue_and_run_correctly_in_activejob end end - test "should enqueue a delivery at a specific time" do + test "should enqueue delivery with a priority" do + job = @mail.deliver_later priority: 10 + assert_equal 10, job.priority + end + + test "should enqueue delivery at a specific time" do later_time = Time.current + 1.hour assert_performed_with(job: ActionMailer::MailDeliveryJob, at: later_time, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]]) do @mail.deliver_later wait_until: later_time end end - test "should enqueue the job on the correct queue" do - assert_performed_with(job: ActionMailer::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]], queue: "test_queue") do + test "should enqueue delivery on the correct queue" do + assert_performed_with(job: ActionMailer::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]], queue: "delayed_mailers") do @mail.deliver_later end end - test "should enqueue the job with the correct delivery job" do + test "should enqueue delivery with the correct job" do old_delivery_job = DelayedMailer.delivery_job DelayedMailer.delivery_job = DummyJob @@ -109,12 +108,26 @@ def test_should_enqueue_and_run_correctly_in_activejob class DummyJob < ActionMailer::MailDeliveryJob; end - test "can override the queue when enqueuing mail" do + test "delivery queue can be overridden when enqueuing mail" do assert_performed_with(job: ActionMailer::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]], queue: "another_queue") do @mail.deliver_later(queue: :another_queue) end end + test "delivery queue can be overridden in subclasses" do + previous_queue_name = DelayedMailer.deliver_later_queue_name + DelayedMailer.deliver_later_queue_name = :throttled_mailers + + assert_equal :throttled_mailers, DelayedMailer.deliver_later_queue_name + assert_equal :mailers, ActionMailer::Base.deliver_later_queue_name + + assert_performed_with(job: ActionMailer::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: []], queue: "throttled_mailers") do + DelayedMailer.test_message.deliver_later + end + ensure + DelayedMailer.deliver_later_queue_name = previous_queue_name + end + test "deliver_later after accessing the message is disallowed" do @mail.message # Load the message, which calls the mailer method. diff --git a/actionmailer/test/parameterized_test.rb b/actionmailer/test/parameterized_test.rb index 62dd671f48ce1..366327ee76be1 100644 --- a/actionmailer/test/parameterized_test.rb +++ b/actionmailer/test/parameterized_test.rb @@ -17,18 +17,13 @@ class DummyDeliveryJob < ActionMailer::MailDeliveryJob @previous_delivery_method = ActionMailer::Base.delivery_method ActionMailer::Base.delivery_method = :test - @previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name - ActionMailer::Base.deliver_later_queue_name = :test_queue - @mail = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com").invitation end teardown do ActiveJob::Base.logger = @previous_logger ParamsMailer.deliveries.clear - ActionMailer::Base.delivery_method = @previous_delivery_method - ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name end test "parameterized headers" do @@ -37,6 +32,13 @@ class DummyDeliveryJob < ActionMailer::MailDeliveryJob assert_equal("So says david@basecamp.com", @mail.body.encoded) end + test "degrade gracefully when .with is not called" do + @mail = ParamsMailer.invitation + + assert_nil(@mail.to) + assert_nil(@mail.from) + end + test "enqueue the email with params" do args = [ "ParamsMailer", diff --git a/actionmailer/test/test_case_test.rb b/actionmailer/test/test_case_test.rb index 7b9647d295c04..9897f3891bb1e 100644 --- a/actionmailer/test/test_case_test.rb +++ b/actionmailer/test/test_case_test.rb @@ -43,7 +43,7 @@ def test_deliveries_are_cleared_on_setup_and_teardown end end -class CrazyNameMailerTest < ActionMailer::TestCase +class ManuallySetNameMailerTest < ActionMailer::TestCase tests TestTestMailer def test_set_mailer_class_manual @@ -51,7 +51,7 @@ def test_set_mailer_class_manual end end -class CrazySymbolNameMailerTest < ActionMailer::TestCase +class ManuallySetSymbolNameMailerTest < ActionMailer::TestCase tests :test_test_mailer def test_set_mailer_class_manual_using_symbol @@ -59,7 +59,7 @@ def test_set_mailer_class_manual_using_symbol end end -class CrazyStringNameMailerTest < ActionMailer::TestCase +class ManuallySetStringNameMailerTest < ActionMailer::TestCase tests "test_test_mailer" def test_set_mailer_class_manual_using_string diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb index f1e55c89a5ae7..de1c3861a96fe 100644 --- a/actionmailer/test/test_helper_test.rb +++ b/actionmailer/test/test_helper_test.rb @@ -7,6 +7,7 @@ class TestHelperMailer < ActionMailer::Base def test @world = "Earth" mail body: render(inline: "Hello, <%= @world %>"), + subject: "Hi!", to: "test@example.com", from: "tester@example.com" end @@ -17,6 +18,12 @@ def test_args(recipient, name) from: "tester@example.com" end + def test_named_args(recipient:, name:) + mail body: render(inline: "Hello, #{name}"), + to: recipient, + from: "tester@example.com" + end + def test_parameter_args mail body: render(inline: "All is #{params[:all]}"), to: "test@example.com", @@ -31,9 +38,21 @@ class CustomDeliveryMailer < TestHelperMailer self.delivery_job = CustomDeliveryJob end +class CustomQueueMailer < TestHelperMailer + self.deliver_later_queue_name = :custom_queue +end + class TestHelperMailerTest < ActionMailer::TestCase include ActiveSupport::Testing::Stream + setup do + @previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name + end + + teardown do + ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name + end + def test_setup_sets_right_action_mailer_options assert_equal :test, ActionMailer::Base.delivery_method assert ActionMailer::Base.perform_deliveries @@ -76,6 +95,26 @@ def test_assert_emails end end + def test_capture_emails + assert_nothing_raised do + emails = capture_emails do + TestHelperMailer.test.deliver_now + end + email = emails.first + assert_instance_of Mail::Message, email + assert_equal "Hello, Earth", email.body.to_s + assert_equal "Hi!", email.subject + + emails = capture_emails do + TestHelperMailer.test.deliver_now + TestHelperMailer.test.deliver_now + end + assert_instance_of Array, emails + assert_instance_of Mail::Message, emails.first + assert_instance_of Mail::Message, emails.second + end + end + def test_assert_emails_with_custom_delivery_job assert_nothing_raised do assert_emails(1) do @@ -206,20 +245,6 @@ def test_assert_enqueued_emails end end - def test_assert_enqueued_emails_with_legacy_delivery_job - previous_delivery_job = TestHelperMailer.delivery_job - TestHelperMailer.delivery_job = ActionMailer::DeliveryJob - assert_nothing_raised do - assert_enqueued_emails 1 do - silence_stream($stdout) do - TestHelperMailer.test.deliver_later - end - end - end - ensure - TestHelperMailer.delivery_job = previous_delivery_job - end - def test_assert_enqueued_parameterized_emails assert_nothing_raised do assert_enqueued_emails 1 do @@ -230,20 +255,6 @@ def test_assert_enqueued_parameterized_emails end end - def test_assert_enqueued_parameterized_emails_with_legacy_delivery_job - previous_delivery_job = TestHelperMailer.delivery_job - TestHelperMailer.delivery_job = ActionMailer::DeliveryJob - assert_nothing_raised do - assert_enqueued_emails 1 do - silence_stream($stdout) do - TestHelperMailer.with(a: 1).test.deliver_later - end - end - end - ensure - TestHelperMailer.delivery_job = previous_delivery_job - end - def test_assert_enqueued_emails_too_few_sent error = assert_raise ActiveSupport::TestCase::Assertion do assert_enqueued_emails 2 do @@ -317,6 +328,70 @@ def test_assert_enqueued_email_with end end + def test_assert_enqueued_email_with_when_deliver_later_queue_name_is_nil + ActionMailer::Base.deliver_later_queue_name = nil + + assert_nothing_raised do + assert_enqueued_email_with TestHelperMailer, :test do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + end + end + end + end + + def test_assert_enqueued_email_with_when_deliver_later_queue_name_with_non_default_name + ActionMailer::Base.deliver_later_queue_name = "sample_mailer_queue_name" + + assert_nothing_raised do + assert_enqueued_email_with TestHelperMailer, :test do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + end + end + end + end + + def test_assert_enqueued_email_with_when_deliver_later_queue_name_is_symbol + ActionMailer::Base.deliver_later_queue_name = :mailers + + assert_nothing_raised do + assert_enqueued_email_with TestHelperMailer, :test do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + end + end + end + end + + def test_assert_enqueued_email_with_when_queue_arg_is_symbol + ActionMailer::Base.deliver_later_queue_name = "mailers" + + assert_nothing_raised do + assert_enqueued_email_with TestHelperMailer, :test, queue: :mailers do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + end + end + end + end + + def test_assert_enqueued_email_with_when_mailer_has_custom_deliver_later_queue + assert_nothing_raised do + assert_enqueued_email_with CustomQueueMailer, :test do + silence_stream($stdout) do + CustomQueueMailer.test.deliver_later + end + end + + assert_enqueued_email_with CustomQueueMailer, :test, queue: :custom_queue do + silence_stream($stdout) do + CustomQueueMailer.test.deliver_later + end + end + end + end + def test_assert_enqueued_email_with_when_mailer_has_custom_delivery_job assert_nothing_raised do assert_enqueued_email_with CustomDeliveryMailer, :test do @@ -357,7 +432,7 @@ def test_assert_enqueued_email_with_with_no_block_with_args def test_assert_enqueued_email_with_with_parameterized_args assert_nothing_raised do - assert_enqueued_email_with TestHelperMailer, :test_parameter_args, args: { all: "good" } do + assert_enqueued_email_with TestHelperMailer, :test_parameter_args, params: { all: "good" } do silence_stream($stdout) do TestHelperMailer.with(all: "good").test_parameter_args.deliver_later end @@ -365,14 +440,172 @@ def test_assert_enqueued_email_with_with_parameterized_args end end + def test_assert_enqueued_email_with_with_parameterized_mailer + assert_nothing_raised do + assert_enqueued_email_with TestHelperMailer.with(all: "good"), :test_parameter_args do + silence_stream($stdout) do + TestHelperMailer.with(all: "good").test_parameter_args.deliver_later + end + end + end + end + + def test_assert_enqueued_email_with_with_named_args + assert_nothing_raised do + assert_enqueued_email_with TestHelperMailer, :test_named_args, args: [{ email: "some_email", name: "some_name" }] do + silence_stream($stdout) do + TestHelperMailer.test_named_args(email: "some_email", name: "some_name").deliver_later + end + end + end + end + + def test_assert_enqueued_email_with_with_params_and_args + assert_nothing_raised do + assert_enqueued_email_with TestHelperMailer, :test_args, params: { all: "good" }, args: ["some_email", "some_name"] do + silence_stream($stdout) do + TestHelperMailer.with(all: "good").test_args("some_email", "some_name").deliver_later + end + end + end + end + + def test_assert_enqueued_email_with_with_params_and_named_args + assert_nothing_raised do + assert_enqueued_email_with TestHelperMailer, :test_named_args, params: { all: "good" }, args: [{ email: "some_email", name: "some_name" }] do + silence_stream($stdout) do + TestHelperMailer.with(all: "good").test_named_args(email: "some_email", name: "some_name").deliver_later + end + end + end + end + def test_assert_enqueued_email_with_with_no_block_with_parameterized_args assert_nothing_raised do silence_stream($stdout) do TestHelperMailer.with(all: "good").test_parameter_args.deliver_later - assert_enqueued_email_with TestHelperMailer, :test_parameter_args, args: { all: "good" } end + assert_enqueued_email_with TestHelperMailer, :test_parameter_args, params: { all: "good" } end end + + def test_assert_enqueued_email_with_supports_params_matcher_proc + mail_params = { all: "good" } + + silence_stream($stdout) do + TestHelperMailer.with(mail_params).test_parameter_args.deliver_later + end + + matcher_params = nil + + assert_nothing_raised do + assert_enqueued_email_with TestHelperMailer, :test_parameter_args, params: ->(params) { matcher_params = params } + end + + assert_equal mail_params, matcher_params + + assert_raises ActiveSupport::TestCase::Assertion do + assert_enqueued_email_with TestHelperMailer, :test_parameter_args, params: ->(_) { false } + end + end + + def test_assert_enqueued_email_with_supports_args_matcher_proc + mail_args = ["some_email", "some_name"] + + silence_stream($stdout) do + TestHelperMailer.test_args(*mail_args).deliver_later + end + + matcher_args = nil + + assert_nothing_raised do + assert_enqueued_email_with TestHelperMailer, :test_args, args: ->(args) { matcher_args = args } + end + + assert_equal mail_args, matcher_args + + assert_raises ActiveSupport::TestCase::Assertion do + assert_enqueued_email_with TestHelperMailer, :test_args, args: ->(_) { false } + end + end + + def test_assert_enqueued_email_with_supports_named_args_matcher_proc + mail_args = [{ email: "some_email", name: "some_name" }] + + silence_stream($stdout) do + TestHelperMailer.test_named_args(**mail_args[0]).deliver_later + end + + matcher_args = nil + + assert_nothing_raised do + assert_enqueued_email_with TestHelperMailer, :test_named_args, args: ->(args) { matcher_args = args } + end + + assert_equal mail_args, matcher_args + end + + def test_deliver_enqueued_emails_with_no_block + assert_nothing_raised do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + deliver_enqueued_emails + end + end + + assert_emails(1) + end + + def test_deliver_enqueued_emails_with_a_block + assert_nothing_raised do + deliver_enqueued_emails do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + end + end + end + + assert_emails(1) + end + + def test_deliver_enqueued_emails_with_custom_delivery_job + assert_nothing_raised do + deliver_enqueued_emails do + silence_stream($stdout) do + CustomDeliveryMailer.test.deliver_later + end + end + end + + assert_emails(1) + end + + def test_deliver_enqueued_emails_with_custom_queue + assert_nothing_raised do + deliver_enqueued_emails(queue: CustomQueueMailer.deliver_later_queue_name) do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + CustomQueueMailer.test.deliver_later + end + end + end + + assert_emails(1) + assert_enqueued_email_with(TestHelperMailer, :test) + end + + def test_deliver_enqueued_emails_with_at + assert_nothing_raised do + deliver_enqueued_emails(at: 1.hour.from_now) do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + TestHelperMailer.test.deliver_later(wait: 2.hours) + end + end + end + + assert_emails(1) + end end class AnotherTestHelperMailerTest < ActionMailer::TestCase @@ -387,3 +620,17 @@ def test_setup_shouldnt_conflict_with_mailer_setup assert_equal "a value", @test_var end end + +class AdapterIsNotTestAdapterTest < ActionMailer::TestCase + def queue_adapter_for_test + ActiveJob::QueueAdapters::InlineAdapter.new + end + + def test_can_send_email_using_any_active_job_adapter + assert_nothing_raised do + assert_emails 1 do + TestHelperMailer.test.deliver_now + end + end + end +end diff --git a/actionmailer/test/url_test.rb b/actionmailer/test/url_test.rb index 5d2a04d8e0fce..123c194aacc2d 100644 --- a/actionmailer/test/url_test.rb +++ b/actionmailer/test/url_test.rb @@ -41,7 +41,7 @@ def exercise_url_for(options) class ActionMailerUrlTest < ActionMailer::TestCase class DummyModel def self.model_name - OpenStruct.new(route_key: "dummy_model") + Struct.new(:route_key, :name).new("dummy_model", nil) end def persisted? diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 346ff39c46510..57413bec8876c 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,276 +1,193 @@ -* Correctly identify the entire localhost IPv4 range as trusted proxy. +* Always check query string keys for valid encoding just like values are checked. - *Nick Soracco* + *Casper Smits* -* `url_for` will now use "https://" as the default protocol when - `Rails.application.config.force_ssl` is set to true. +* Always return empty body for HEAD requests in `PublicExceptions` and + `DebugExceptions`. - *Jonathan Hefner* + This is required by `Rack::Lint` (per RFC9110). -* Accept and default to base64_urlsafe CSRF tokens. + *Hartley McGuire* - Base64 strict-encoded CSRF tokens are not inherently websafe, which makes - them difficult to deal with. For example, the common practice of sending - the CSRF token to a browser in a client-readable cookie does not work properly - out of the box: the value has to be url-encoded and decoded to survive transport. +* Add comprehensive support for HTTP Cache-Control request directives according to RFC 9111. - Now, we generate Base64 urlsafe-encoded CSRF tokens, which are inherently safe - to transport. Validation accepts both urlsafe tokens, and strict-encoded tokens - for backwards compatibility. + Provides a `request.cache_control_directives` object that gives access to request cache directives: - *Scott Blum* - -* Support rolling deploys for cookie serialization/encryption changes. - - In a distributed configuration like rolling update, users may observe - both old and new instances during deployment. Users may be served by a - new instance and then by an old instance. - - That means when the server changes `cookies_serializer` from `:marshal` - to `:hybrid` or the server changes `use_authenticated_cookie_encryption` - from `false` to `true`, users may lose their sessions if they access the - server during deployment. - - We added fallbacks to downgrade the cookie format when necessary during - deployment, ensuring compatibility on both old and new instances. - - *Masaki Hara* - -* `ActionDispatch::Request.remote_ip` has ip address even when all sites are trusted. - - Before, if all `X-Forwarded-For` sites were trusted, the `remote_ip` would default to `127.0.0.1`. - Now, the furthest proxy site is used. e.g.: It now gives an ip address when using curl from the load balancer. - - *Keenan Brock* - -* Fix possible information leak / session hijacking vulnerability. - - The `ActionDispatch::Session::MemcacheStore` is still vulnerable given it requires the - gem dalli to be updated as well. - - CVE-2019-16782. - -* Include child session assertion count in ActionDispatch::IntegrationTest. - - `IntegrationTest#open_session` uses `dup` to create the new session, which - meant it had its own copy of `@assertions`. This prevented the assertions - from being correctly counted and reported. - - Child sessions now have their `attr_accessor` overridden to delegate to the - root session. - - Fixes #32142. - - *Sam Bostock* - -* Add SameSite protection to every written cookie. - - Enabling `SameSite` cookie protection is an addition to CSRF protection, - where cookies won't be sent by browsers in cross-site POST requests when set to `:lax`. - - `:strict` disables cookies being sent in cross-site GET or POST requests. - - Passing `:none` disables this protection and is the same as previous versions albeit a `; SameSite=None` is appended to the cookie. - - See upgrade instructions in config/initializers/new_framework_defaults_6_1.rb. - - More info [here](https://tools.ietf.org/html/draft-west-first-party-cookies-07) - - _NB: Technically already possible as Rack supports SameSite protection, this is to ensure it's applied to all cookies_ - - *Cédric Fabianski* - -* Bring back the feature that allows loading external route files from the router. + ```ruby + # Boolean directives + request.cache_control_directives.only_if_cached? # => true/false + request.cache_control_directives.no_cache? # => true/false + request.cache_control_directives.no_store? # => true/false + request.cache_control_directives.no_transform? # => true/false + + # Value directives + request.cache_control_directives.max_age # => integer or nil + request.cache_control_directives.max_stale # => integer or nil (or true for valueless max-stale) + request.cache_control_directives.min_fresh # => integer or nil + request.cache_control_directives.stale_if_error # => integer or nil + + # Special helpers for max-stale + request.cache_control_directives.max_stale? # => true if max-stale present (with or without value) + request.cache_control_directives.max_stale_unlimited? # => true only for valueless max-stale + ``` - This feature existed back in 2012 but got reverted with the incentive that - https://github.com/rails/routing_concerns was a better approach. Turned out - that this wasn't fully the case and loading external route files from the router - can be helpful for applications with a really large set of routes. - Without this feature, application needs to implement routes reloading - themselves and it's not straightforward. + Example usage: ```ruby - # config/routes.rb + def show + if request.cache_control_directives.only_if_cached? + @article = Article.find_cached(params[:id]) + return head(:gateway_timeout) if @article.nil? + else + @article = Article.find(params[:id]) + end - Rails.application.routes.draw do - draw(:admin) + render :show end - - # config/routes/admin.rb - - get :foo, to: 'foo#bar' ``` - *Yehuda Katz*, *Edouard Chin* + *egg528* -* Fix system test driver option initialization for non-headless browsers. +* Add assert_in_body/assert_not_in_body as the simplest way to check if a piece of text is in the response body. - *glaszig* + *DHH* -* `redirect_to.action_controller` notifications now include the `ActionDispatch::Request` in - their payloads as `:request`. +* Include cookie name when calculating maximum allowed size. - *Austin Story* + *Hartley McGuire* -* `respond_to#any` no longer returns a response's Content-Type based on the - request format but based on the block given. +* Implement `must-understand` directive according to RFC 9111. - Example: + The `must-understand` directive indicates that a cache must understand the semantics of the response status code, or discard the response. This directive is enforced to be used only with `no-store` to ensure proper cache behavior. ```ruby - def my_action - respond_to do |format| - format.any { render(json: { foo: 'bar' }) } + class ArticlesController < ApplicationController + def show + @article = Article.find(params[:id]) + + if @article.special_format? + must_understand + render status: 203 # Non-Authoritative Information + else + fresh_when @article end end - - get('my_action.csv') + end ``` - The previous behaviour was to respond with a `text/csv` Content-Type which - is inaccurate since a JSON response is being rendered. - - Now it correctly returns a `application/json` Content-Type. - - *Edouard Chin* + *heka1024* -* Replaces (back)slashes in failure screenshot image paths with dashes. +* The JSON renderer doesn't escape HTML entities or Unicode line separators anymore. - If a failed test case contained a slash or a backslash, a screenshot would be created in a - nested directory, causing issues with `tmp:clear`. + Using `render json:` will no longer escape `<`, `>`, `&`, `U+2028` and `U+2029` characters that can cause errors + when the resulting JSON is embedded in JavaScript, or vulnerabilities when the resulting JSON is embedded in HTML. - *Damir Zekic* + Since the renderer is used to return a JSON document as `application/json`, it's typically not necessary to escape + those characters, and it improves performance. -* Add `params.member?` to mimic Hash behavior. + Escaping will still occur when the `:callback` option is set, since the JSON is used as JavaScript code in this + situation (JSONP). - *Younes Serraj* + You can use the `:escape` option or set `config.action_controller.escape_json_responses` to `true` to restore the + escaping behavior. -* `process_action.action_controller` notifications now include the following in their payloads: + ```ruby + class PostsController < ApplicationController + def index + render json: Post.last(30), escape: true + end + end + ``` - * `:request` - the `ActionDispatch::Request` - * `:response` - the `ActionDispatch::Response` + *Étienne Barrié*, *Jean Boussier* - *George Claghorn* +* Load lazy route sets before inserting test routes -* Updated `ActionDispatch::Request.remote_ip` setter to clear set the instance - `remote_ip` to `nil` before setting the header that the value is derived - from. + Without loading lazy route sets early, we miss `after_routes_loaded` callbacks, or risk + invoking them with the test routes instead of the real ones if another load is triggered by an engine. - Fixes #37383. + *Gannon McGibbon* - *Norm Provost* +* Raise `AbstractController::DoubleRenderError` if `head` is called after rendering. -* `ActionController::Base.log_at` allows setting a different log level per request. + After this change, invoking `head` will lead to an error if response body is already set: ```ruby - # Use the debug level if a particular cookie is set. - class ApplicationController < ActionController::Base - log_at :debug, if: -> { cookies[:debug] } + class PostController < ApplicationController + def index + render locals: {} + head :ok + end end ``` - *George Claghorn* - -* Allow system test screen shots to be taken more than once in - a test by prefixing the file name with an incrementing counter. - - Add an environment variable `RAILS_SYSTEM_TESTING_SCREENSHOT_HTML` to - enable saving of HTML during a screenshot in addition to the image. - This uses the same image name, with the extension replaced with `.html` - - *Tom Fakes* - -* Add `Vary: Accept` header when using `Accept` header for response. + *Iaroslav Kurbatov* - For some requests like `/users/1`, Rails uses requests' `Accept` - header to determine what to return. And if we don't add `Vary` - in the response header, browsers might accidentally cache different - types of content, which would cause issues: e.g. javascript got displayed - instead of html content. This PR fixes these issues by adding `Vary: Accept` - in these types of requests. For more detailed problem description, please read: +* The Cookie Serializer can now serialize an Active Support SafeBuffer when using message pack. - https://github.com/rails/rails/pull/36213 + Such code would previously produce an error if an application was using messagepack as its cookie serializer. - Fixes #25842. - - *Stan Lo* - -* Fix IntegrationTest `follow_redirect!` to follow redirection using the same HTTP verb when following - a 307 redirection. + ```ruby + class PostController < ApplicationController + def index + flash.notice = t(:hello_html) # This would try to serialize a SafeBuffer, which was not possible. + end + end + ``` *Edouard Chin* -* System tests require Capybara 3.26 or newer. - - *George Claghorn* - -* Reduced log noise handling ActionController::RoutingErrors. +* Fix `Rails.application.reload_routes!` from clearing almost all routes. - *Alberto Fernández-Capel* + When calling `Rails.application.reload_routes!` inside a middleware of + a Rake task, it was possible under certain conditions that all routes would be cleared. + If ran inside a middleware, this would result in getting a 404 on most page you visit. + This issue was only happening in development. -* Add DSL for configuring HTTP Feature Policy. - - This new DSL provides a way to configure an HTTP Feature Policy at a - global or per-controller level. Full details of HTTP Feature Policy - specification and guidelines can be found at MDN: + *Edouard Chin* - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy +* Add resource name to the `ArgumentError` that's raised when invalid `:only` or `:except` options are given to `#resource` or `#resources` - Example global policy: + This makes it easier to locate the source of the problem, especially for routes drawn by gems. - ```ruby - Rails.application.config.feature_policy do |f| - f.camera :none - f.gyroscope :none - f.microphone :none - f.usb :none - f.fullscreen :self - f.payment :self, "https://secure.example.com" - end + Before: ``` - - Example controller level policy: - - ```ruby - class PagesController < ApplicationController - feature_policy do |p| - p.geolocation "https://example.com" - end - end + :only and :except must include only [:index, :create, :new, :show, :update, :destroy, :edit], but also included [:foo, :bar] ``` - *Jacob Bednarz* - -* Add the ability to set the CSP nonce only to the specified directives. - - Fixes #35137. - - *Yuji Yaginuma* - -* Keep part when scope option has value. + After: + ``` + Route `resources :products` - :only and :except must include only [:index, :create, :new, :show, :update, :destroy, :edit], but also included [:foo, :bar] + ``` - When a route was defined within an optional scope, if that route didn't - take parameters the scope was lost when using path helpers. This commit - ensures scope is kept both when the route takes parameters or when it - doesn't. + *Jeremy Green* - Fixes #33219. +* Add `check_collisions` option to `ActionDispatch::Session::CacheStore`. - *Alberto Almagro* + Newly generated session ids use 128 bits of randomness, which is more than + enough to ensure collisions can't happen, but if you need to harden sessions + even more, you can enable this option to check in the session store that the id + is indeed free you can enable that option. This however incurs an extra write + on session creation. -* Added `deep_transform_keys` and `deep_transform_keys!` methods to ActionController::Parameters. + *Shia* - *Gustavo Gutierrez* +* In ExceptionWrapper, match backtrace lines with built templates more often, + allowing improved highlighting of errors within do-end blocks in templates. + Fix for Ruby 3.4 to match new method labels in backtrace. -* Calling `ActionController::Parameters#transform_keys`/`!` without a block now returns - an enumerator for the parameters instead of the underlying hash. + *Martin Emde* - *Eugene Kenny* +* Allow setting content type with a symbol of the Mime type. -* Fix strong parameters blocks all attributes even when only some keys are invalid (non-numerical). - It should only block invalid key's values instead. + ```ruby + # Before + response.content_type = "text/html" - *Stan Lo* + # After + response.content_type = :html + ``` + *Petrik de Heus* -Please check [6-0-stable](https://github.com/rails/rails/blob/6-0-stable/actionpack/CHANGELOG.md) for previous changes. +Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/actionpack/CHANGELOG.md) for previous changes. diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE index 0e2be7cd7f0d7..7be9ac633faf0 100644 --- a/actionpack/MIT-LICENSE +++ b/actionpack/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2020 David Heinemeier Hansson +Copyright (c) David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc index 67d0c62a84815..028f5910d383a 100644 --- a/actionpack/README.rdoc +++ b/actionpack/README.rdoc @@ -2,9 +2,8 @@ Action Pack is a framework for handling and responding to web requests. It provides mechanisms for *routing* (mapping request URLs to actions), defining -*controllers* that implement actions, and generating responses by rendering -*views*, which are templates of various formats. In short, Action Pack -provides the view and controller layers in the MVC paradigm. +*controllers* that implement actions, and generating responses. In short, Action Pack +provides the controller layer in the MVC paradigm. It consists of several modules: @@ -17,11 +16,11 @@ It consists of several modules: subclassed to implement filters and actions to handle requests. The result of an action is typically content generated from views. -With the Ruby on Rails framework, users only directly interface with the +With the Ruby on \Rails framework, users only directly interface with the Action Controller module. Necessary Action Dispatch functionality is activated by default and Action View rendering is implicitly triggered by Action Controller. However, these modules are designed to function on their own and -can be used outside of Rails. +can be used outside of \Rails. You can read more about Action Pack in the {Action Controller Overview}[https://guides.rubyonrails.org/action_controller_overview.html] guide. @@ -31,9 +30,9 @@ The latest version of Action Pack can be installed with RubyGems: $ gem install actionpack -Source code can be downloaded as part of the Rails project on GitHub: +Source code can be downloaded as part of the \Rails project on GitHub: -* https://github.com/rails/rails/tree/master/actionpack +* https://github.com/rails/rails/tree/main/actionpack == License @@ -49,7 +48,7 @@ API documentation is at: * https://api.rubyonrails.org -Bug reports for the Ruby on Rails project can be filed here: +Bug reports for the Ruby on \Rails project can be filed here: * https://github.com/rails/rails/issues diff --git a/actionpack/Rakefile b/actionpack/Rakefile index cddc536d3b7bc..1ee11d7f1c509 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -7,7 +7,7 @@ test_files = FileList["test/**/*_test.rb"] desc "Default Task" task default: :test -task :package +ENV["RAILS_MINITEST_PLUGIN"] = "true" # Run the unit tests Rake::TestTask.new do |t| @@ -16,6 +16,7 @@ Rake::TestTask.new do |t| t.warning = true t.verbose = true + t.options = "--profile" if ENV["CI"] t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 332f861acda1b..053897cc7dbcd 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.summary = "Web-flow and rendering framework putting the VC in MVC (part of Rails)." s.description = "Web apps on Rails. Simple, battle-tested conventions for building and testing MVC web applications. Works with any Rack-compatible server." - s.required_ruby_version = ">= 2.5.0" + s.required_ruby_version = ">= 3.2.0" s.license = "MIT" @@ -27,6 +27,7 @@ Gem::Specification.new do |s| "documentation_uri" => "https://api.rubyonrails.org/v#{version}/", "mailing_list_uri" => "https://discuss.rubyonrails.org/c/rubyonrails-talk", "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actionpack", + "rubygems_mfa_required" => "true", } # NOTE: Please read our dependency guidelines before updating versions: @@ -34,10 +35,13 @@ Gem::Specification.new do |s| s.add_dependency "activesupport", version - s.add_dependency "rack", "~> 2.0", ">= 2.0.8" + s.add_dependency "nokogiri", ">= 1.8.5" + s.add_dependency "rack", ">= 2.2.4" + s.add_dependency "rack-session", ">= 1.0.1" s.add_dependency "rack-test", ">= 0.6.3" - s.add_dependency "rails-html-sanitizer", "~> 1.0", ">= 1.2.0" - s.add_dependency "rails-dom-testing", "~> 2.0" + s.add_dependency "rails-html-sanitizer", "~> 1.6" + s.add_dependency "rails-dom-testing", "~> 2.2" + s.add_dependency "useragent", "~> 0.16" s.add_dependency "actionview", version s.add_development_dependency "activemodel", version diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index d1ff62a032fb5..6b3004045b4dd 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -1,9 +1,12 @@ # frozen_string_literal: true +# :markup: markdown + require "action_pack" require "active_support" require "active_support/rails" require "active_support/i18n" +require "abstract_controller/deprecator" module AbstractController extend ActiveSupport::Autoload @@ -24,5 +27,10 @@ module AbstractController def self.eager_load! super AbstractController::Caching.eager_load! + AbstractController::Base.descendants.each do |controller| + unless controller.abstract? + controller.eager_load! + end + end end end diff --git a/actionpack/lib/abstract_controller/asset_paths.rb b/actionpack/lib/abstract_controller/asset_paths.rb index d6ee84b87bbcf..44e30fb917bba 100644 --- a/actionpack/lib/abstract_controller/asset_paths.rb +++ b/actionpack/lib/abstract_controller/asset_paths.rb @@ -1,12 +1,16 @@ # frozen_string_literal: true +# :markup: markdown + module AbstractController - module AssetPaths #:nodoc: + module AssetPaths # :nodoc: extend ActiveSupport::Concern included do - config_accessor :asset_host, :assets_dir, :javascripts_dir, - :stylesheets_dir, :default_asset_host_protocol, :relative_url_root + singleton_class.delegate :asset_host, :asset_host=, :assets_dir, :assets_dir=, :javascripts_dir, :javascripts_dir=, + :stylesheets_dir, :stylesheets_dir=, :default_asset_host_protocol, :default_asset_host_protocol=, :relative_url_root, :relative_url_root=, to: :config + delegate :asset_host, :asset_host=, :assets_dir, :assets_dir=, :javascripts_dir, :javascripts_dir=, + :stylesheets_dir, :stylesheets_dir=, :default_asset_host_protocol, :default_asset_host_protocol=, :relative_url_root, :relative_url_root=, to: :config end end end diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index 3ff922029b664..c16601d35fa65 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true +# :markup: markdown + require "abstract_controller/error" -require "active_support/configurable" require "active_support/descendants_tracker" require "active_support/core_ext/module/anonymous" require "active_support/core_ext/module/attr_internal" @@ -9,12 +10,29 @@ module AbstractController # Raised when a non-existing controller action is triggered. class ActionNotFound < StandardError + attr_reader :controller, :action # :nodoc: + + def initialize(message = nil, controller = nil, action = nil) # :nodoc: + @controller = controller + @action = action + super(message) + end + + if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker) + include DidYouMean::Correctable # :nodoc: + + def corrections # :nodoc: + @corrections ||= DidYouMean::SpellChecker.new(dictionary: controller.class.action_methods).correct(action) + end + end end - # AbstractController::Base is a low-level API. Nobody should be - # using it directly, and subclasses (like ActionController::Base) are - # expected to provide their own +render+ method, since rendering means - # different things depending on the context. + # # Abstract Controller Base + # + # AbstractController::Base is a low-level API. Nobody should be using it + # directly, and subclasses (like ActionController::Base) are expected to provide + # their own `render` method, since rendering means different things depending on + # the context. class Base ## # Returns the body of the HTTP response sent by the controller. @@ -28,83 +46,90 @@ class Base # Returns the formats that can be processed by the controller. attr_internal :formats - include ActiveSupport::Configurable + class_attribute :config, instance_predicate: false, default: ActiveSupport::OrderedOptions.new extend ActiveSupport::DescendantsTracker class << self attr_reader :abstract alias_method :abstract?, :abstract - # Define a controller as abstract. See internal_methods for more - # details. + # Define a controller as abstract. See internal_methods for more details. def abstract! @abstract = true end def inherited(klass) # :nodoc: - # Define the abstract ivar on subclasses so that we don't get - # uninitialized ivar warnings + # Define the abstract ivar on subclasses so that we don't get uninitialized ivar + # warnings unless klass.instance_variable_defined?(:@abstract) klass.instance_variable_set(:@abstract, false) end + klass.config = ActiveSupport::InheritableOptions.new(config) super end - # A list of all internal methods for a controller. This finds the first - # abstract superclass of a controller, and gets a list of all public - # instance methods on that abstract class. Public instance methods of - # a controller would normally be considered action methods, so methods - # declared on abstract classes are being removed. - # (ActionController::Metal and ActionController::Base are defined as abstract) + # A list of all internal methods for a controller. This finds the first abstract + # superclass of a controller, and gets a list of all public instance methods on + # that abstract class. Public instance methods of a controller would normally be + # considered action methods, so methods declared on abstract classes are being + # removed. (ActionController::Metal and ActionController::Base are defined as + # abstract) def internal_methods controller = self + methods = [] - controller = controller.superclass until controller.abstract? - controller.public_instance_methods(true) + until controller.abstract? + methods += controller.public_instance_methods(false) + controller = controller.superclass + end + + controller.public_instance_methods(true) - methods end - # A list of method names that should be considered actions. This - # includes all public instance methods on a controller, less - # any internal methods (see internal_methods), adding back in - # any methods that are internal, but still exist on the class - # itself. + # A list of method names that should be considered actions. This includes all + # public instance methods on a controller, less any internal methods (see + # internal_methods), adding back in any methods that are internal, but still + # exist on the class itself. + # + # #### Returns + # * `Set` - A set of all methods that should be considered actions. # - # ==== Returns - # * Set - A set of all methods that should be considered actions. def action_methods @action_methods ||= begin - # All public instance methods of this class, including ancestors - methods = (public_instance_methods(true) - - # Except for public instance methods of Base and its ancestors - internal_methods + - # Be sure to include shadowed public instance methods of this class - public_instance_methods(false)) - + # All public instance methods of this class, including ancestors except for + # public instance methods of Base and its ancestors. + methods = public_instance_methods(true) - internal_methods + # Be sure to include shadowed public instance methods of this class. + methods.concat(public_instance_methods(false)) methods.map!(&:to_s) - methods.to_set end end - # action_methods are cached and there is sometimes a need to refresh - # them. ::clear_action_methods! allows you to do that, so next time - # you run action_methods, they will be recalculated. + # action_methods are cached and there is sometimes a need to refresh them. + # ::clear_action_methods! allows you to do that, so next time you run + # action_methods, they will be recalculated. def clear_action_methods! @action_methods = nil end # Returns the full controller name, underscored, without the ending Controller. # - # class MyApp::MyPostsController < AbstractController::Base + # class MyApp::MyPostsController < AbstractController::Base + # + # end # - # end + # MyApp::MyPostsController.controller_path # => "my_app/my_posts" # - # MyApp::MyPostsController.controller_path # => "my_app/my_posts" + # #### Returns + # * `String` # - # ==== Returns - # * String def controller_path - @controller_path ||= name.sub(/Controller$/, "").underscore unless anonymous? + @controller_path ||= name.delete_suffix("Controller").underscore unless anonymous? + end + + def configure # :nodoc: + yield config end # Refresh the cached action_methods when a new action_method is added. @@ -112,144 +137,160 @@ def method_added(name) super clear_action_methods! end + + def eager_load! # :nodoc: + action_methods + nil + end end abstract! - # Calls the action going through the entire action dispatch stack. + # Calls the action going through the entire Action Dispatch stack. + # + # The actual method that is called is determined by calling #method_for_action. + # If no method can handle the action, then an AbstractController::ActionNotFound + # error is raised. # - # The actual method that is called is determined by calling - # #method_for_action. If no method can handle the action, then an - # AbstractController::ActionNotFound error is raised. + # #### Returns + # * `self` # - # ==== Returns - # * self - def process(action, *args) + def process(action, ...) @_action_name = action.to_s unless action_name = _find_action_name(@_action_name) - raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}" + raise ActionNotFound.new("The action '#{action}' could not be found for #{self.class.name}", self, action) end @_response_body = nil - process_action(action_name, *args) + process_action(action_name, ...) end - # Delegates to the class' ::controller_path + # Delegates to the class's ::controller_path. def controller_path self.class.controller_path end - # Delegates to the class' ::action_methods + # Delegates to the class's ::action_methods. def action_methods self.class.action_methods end - # Returns true if a method for the action is available and - # can be dispatched, false otherwise. + # Returns true if a method for the action is available and can be dispatched, + # false otherwise. # - # Notice that action_methods.include?("foo") may return - # false and available_action?("foo") returns true because - # this method considers actions that are also available - # through other means, for example, implicit render ones. + # Notice that `action_methods.include?("foo")` may return false and + # `available_action?("foo")` returns true because this method considers actions + # that are also available through other means, for example, implicit render + # ones. + # + # #### Parameters + # * `action_name` - The name of an action to be tested # - # ==== Parameters - # * action_name - The name of an action to be tested def available_action?(action_name) _find_action_name(action_name) end - # Tests if a response body is set. Used to determine if the - # +process_action+ callback needs to be terminated in - # +AbstractController::Callbacks+. + # Tests if a response body is set. Used to determine if the `process_action` + # callback needs to be terminated in AbstractController::Callbacks. def performed? response_body end - # Returns true if the given controller is capable of rendering - # a path. A subclass of +AbstractController::Base+ - # may return false. An Email controller for example does not - # support paths, only full URLs. + # Returns true if the given controller is capable of rendering a path. A + # subclass of `AbstractController::Base` may return false. An Email controller + # for example does not support paths, only full URLs. def self.supports_path? true end + def config # :nodoc: + @_config ||= self.class.config.inheritable_copy + end + + def inspect # :nodoc: + "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>" + end + private - # Returns true if the name can be considered an action because - # it has a method defined in the controller. + # Returns true if the name can be considered an action because it has a method + # defined in the controller. + # + # #### Parameters + # * `name` - The name of an action to be tested # - # ==== Parameters - # * name - The name of an action to be tested def action_method?(name) self.class.action_methods.include?(name) end - # Call the action. Override this in a subclass to modify the - # behavior around processing an action. This, and not #process, - # is the intended way to override action dispatching. + # Call the action. Override this in a subclass to modify the behavior around + # processing an action. This, and not #process, is the intended way to override + # action dispatching. # - # Notice that the first argument is the method to be dispatched - # which is *not* necessarily the same as the action name. - def process_action(method_name, *args) - send_action(method_name, *args) + # Notice that the first argument is the method to be dispatched which is **not** + # necessarily the same as the action name. + def process_action(...) + send_action(...) end - # Actually call the method associated with the action. Override - # this method if you wish to change how action methods are called, - # not to add additional behavior around it. For example, you would - # override #send_action if you want to inject arguments into the - # method. + # Actually call the method associated with the action. Override this method if + # you wish to change how action methods are called, not to add additional + # behavior around it. For example, you would override #send_action if you want + # to inject arguments into the method. alias send_action send - # If the action name was not found, but a method called "action_missing" - # was found, #method_for_action will return "_handle_action_missing". - # This method calls #action_missing with the current action name. + # If the action name was not found, but a method called "action_missing" was + # found, #method_for_action will return "_handle_action_missing". This method + # calls #action_missing with the current action name. def _handle_action_missing(*args) action_missing(@_action_name, *args) end - # Takes an action name and returns the name of the method that will - # handle the action. + # Takes an action name and returns the name of the method that will handle the + # action. # # It checks if the action name is valid and returns false otherwise. # # See method_for_action for more information. # - # ==== Parameters - # * action_name - An action name to find a method name for + # #### Parameters + # * `action_name` - An action name to find a method name for + # # - # ==== Returns - # * string - The name of the method that handles the action - # * false - No valid method name could be found. - # Raise +AbstractController::ActionNotFound+. + # #### Returns + # * `string` - The name of the method that handles the action + # * false - No valid method name could be found. + # + # Raise `AbstractController::ActionNotFound`. def _find_action_name(action_name) _valid_action_name?(action_name) && method_for_action(action_name) end - # Takes an action name and returns the name of the method that will - # handle the action. In normal cases, this method returns the same - # name as it receives. By default, if #method_for_action receives - # a name that is not an action, it will look for an #action_missing - # method and return "_handle_action_missing" if one is found. + # Takes an action name and returns the name of the method that will handle the + # action. In normal cases, this method returns the same name as it receives. By + # default, if #method_for_action receives a name that is not an action, it will + # look for an #action_missing method and return "_handle_action_missing" if one + # is found. + # + # Subclasses may override this method to add additional conditions that should + # be considered an action. For instance, an HTTP controller with a template + # matching the action name is considered to exist. + # + # If you override this method to handle additional cases, you may also provide a + # method (like `_handle_method_missing`) to handle the case. # - # Subclasses may override this method to add additional conditions - # that should be considered an action. For instance, an HTTP controller - # with a template matching the action name is considered to exist. + # If none of these conditions are true, and `method_for_action` returns `nil`, + # an `AbstractController::ActionNotFound` exception will be raised. # - # If you override this method to handle additional cases, you may - # also provide a method (like +_handle_method_missing+) to handle - # the case. + # #### Parameters + # * `action_name` - An action name to find a method name for # - # If none of these conditions are true, and +method_for_action+ - # returns +nil+, an +AbstractController::ActionNotFound+ exception will be raised. # - # ==== Parameters - # * action_name - An action name to find a method name for + # #### Returns + # * `string` - The name of the method that handles the action + # * `nil` - No method name could be found. # - # ==== Returns - # * string - The name of the method that handles the action - # * nil - No method name could be found. def method_for_action(action_name) if action_method?(action_name) action_name diff --git a/actionpack/lib/abstract_controller/caching.rb b/actionpack/lib/abstract_controller/caching.rb index 5e9cb4d2f9c81..2b976395fede0 100644 --- a/actionpack/lib/abstract_controller/caching.rb +++ b/actionpack/lib/abstract_controller/caching.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module AbstractController module Caching extend ActiveSupport::Concern @@ -30,13 +32,16 @@ def cache_configured? included do extend ConfigMethods - config_accessor :default_static_extension + singleton_class.delegate :default_static_extension, :default_static_extension=, to: :config + delegate :default_static_extension, :default_static_extension=, to: :config self.default_static_extension ||= ".html" - config_accessor :perform_caching + singleton_class.delegate :perform_caching, :perform_caching=, to: :config + delegate :perform_caching, :perform_caching=, to: :config self.perform_caching = true if perform_caching.nil? - config_accessor :enable_fragment_cache_logging + singleton_class.delegate :enable_fragment_cache_logging, :enable_fragment_cache_logging=, to: :config + delegate :enable_fragment_cache_logging, :enable_fragment_cache_logging=, to: :config self.enable_fragment_cache_logging = false class_attribute :_view_cache_dependencies, default: [] @@ -50,7 +55,7 @@ def view_cache_dependency(&dependency) end def view_cache_dependencies - self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact + self.class._view_cache_dependencies.filter_map { |dep| instance_exec(&dep) } end private diff --git a/actionpack/lib/abstract_controller/caching/fragments.rb b/actionpack/lib/abstract_controller/caching/fragments.rb index 18677ddd18ee5..8998e204aa6a5 100644 --- a/actionpack/lib/abstract_controller/caching/fragments.rb +++ b/actionpack/lib/abstract_controller/caching/fragments.rb @@ -1,20 +1,23 @@ # frozen_string_literal: true +# :markup: markdown + module AbstractController module Caching - # Fragment caching is used for caching various blocks within - # views without caching the entire action as a whole. This is - # useful when certain elements of an action change frequently or - # depend on complicated state while other parts rarely change or - # can be shared amongst multiple parties. The caching is done using - # the +cache+ helper available in the Action View. See + # # Abstract Controller Caching Fragments + # + # Fragment caching is used for caching various blocks within views without + # caching the entire action as a whole. This is useful when certain elements of + # an action change frequently or depend on complicated state while other parts + # rarely change or can be shared amongst multiple parties. The caching is done + # using the `cache` helper available in the Action View. See # ActionView::Helpers::CacheHelper for more information. # - # While it's strongly recommended that you use key-based cache - # expiration (see links in CacheHelper for more information), - # it is also possible to manually expire caches. For example: + # While it's strongly recommended that you use key-based cache expiration (see + # links in CacheHelper for more information), it is also possible to manually + # expire caches. For example: # - # expire_fragment('name_of_cache') + # expire_fragment('name_of_cache') module Fragments extend ActiveSupport::Concern @@ -33,38 +36,35 @@ module Fragments end module ClassMethods - # Allows you to specify controller-wide key prefixes for - # cache fragments. Pass either a constant +value+, or a block - # which computes a value each time a cache key is generated. + # Allows you to specify controller-wide key prefixes for cache fragments. Pass + # either a constant `value`, or a block which computes a value each time a cache + # key is generated. # - # For example, you may want to prefix all fragment cache keys - # with a global version identifier, so you can easily - # invalidate all caches. + # For example, you may want to prefix all fragment cache keys with a global + # version identifier, so you can easily invalidate all caches. # - # class ApplicationController - # fragment_cache_key "v1" - # end + # class ApplicationController + # fragment_cache_key "v1" + # end # - # When it's time to invalidate all fragments, simply change - # the string constant. Or, progressively roll out the cache - # invalidation using a computed value: + # When it's time to invalidate all fragments, simply change the string constant. + # Or, progressively roll out the cache invalidation using a computed value: # - # class ApplicationController - # fragment_cache_key do - # @account.id.odd? ? "v1" : "v2" + # class ApplicationController + # fragment_cache_key do + # @account.id.odd? ? "v1" : "v2" + # end # end - # end def fragment_cache_key(value = nil, &key) self.fragment_cache_keys += [key || -> { value }] end end - # Given a key (as described in +expire_fragment+), returns - # a key array suitable for use in reading, writing, or expiring a - # cached fragment. All keys begin with :views, - # followed by ENV["RAILS_CACHE_ID"] or ENV["RAILS_APP_VERSION"] if set, - # followed by any controller-wide key prefix values, ending - # with the specified +key+ value. + # Given a key (as described in `expire_fragment`), returns a key array suitable + # for use in reading, writing, or expiring a cached fragment. All keys begin + # with `:views`, followed by `ENV["RAILS_CACHE_ID"]` or + # `ENV["RAILS_APP_VERSION"]` if set, followed by any controller-wide key prefix + # values, ending with the specified `key` value. def combined_fragment_cache_key(key) head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } tail = key.is_a?(Hash) ? url_for(key).split("://").last : key @@ -75,8 +75,8 @@ def combined_fragment_cache_key(key) cache_key end - # Writes +content+ to the location signified by - # +key+ (see +expire_fragment+ for acceptable formats). + # Writes `content` to the location signified by `key` (see `expire_fragment` for + # acceptable formats). def write_fragment(key, content, options = nil) return content unless cache_configured? @@ -88,8 +88,8 @@ def write_fragment(key, content, options = nil) content end - # Reads a cached fragment from the location signified by +key+ - # (see +expire_fragment+ for acceptable formats). + # Reads a cached fragment from the location signified by `key` (see + # `expire_fragment` for acceptable formats). def read_fragment(key, options = nil) return unless cache_configured? @@ -100,8 +100,8 @@ def read_fragment(key, options = nil) end end - # Check if a cached fragment from the location signified by - # +key+ exists (see +expire_fragment+ for acceptable formats). + # Check if a cached fragment from the location signified by `key` exists (see + # `expire_fragment` for acceptable formats). def fragment_exist?(key, options = nil) return unless cache_configured? key = combined_fragment_cache_key(key) @@ -113,22 +113,21 @@ def fragment_exist?(key, options = nil) # Removes fragments from the cache. # - # +key+ can take one of three forms: + # `key` can take one of three forms: + # + # * String - This would normally take the form of a path, like + # `pages/45/notes`. + # * Hash - Treated as an implicit call to `url_for`, like `{ controller: + # 'pages', action: 'notes', id: 45}` + # * Regexp - Will remove any fragment that matches, so `%r{pages/\d*/notes}` + # might remove all notes. Make sure you don't use anchors in the regex (`^` + # or `$`) because the actual filename matched looks like + # `./cache/filename/path.cache`. Note: Regexp expiration is only supported + # on caches that can iterate over all keys (unlike memcached). # - # * String - This would normally take the form of a path, like - # pages/45/notes. - # * Hash - Treated as an implicit call to +url_for+, like - # { controller: 'pages', action: 'notes', id: 45} - # * Regexp - Will remove any fragment that matches, so - # %r{pages/\d*/notes} might remove all notes. Make sure you - # don't use anchors in the regex (^ or $) because - # the actual filename matched looks like - # ./cache/filename/path.cache. Note: Regexp expiration is - # only supported on caches that can iterate over all keys (unlike - # memcached). # - # +options+ is passed through to the cache store's +delete+ - # method (or delete_matched, for Regexp keys). + # `options` is passed through to the cache store's `delete` method (or + # `delete_matched`, for Regexp keys). def expire_fragment(key, options = nil) return unless cache_configured? key = combined_fragment_cache_key(key) unless key.is_a?(Regexp) @@ -142,8 +141,8 @@ def expire_fragment(key, options = nil) end end - def instrument_fragment_cache(name, key) # :nodoc: - ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key)) { yield } + def instrument_fragment_cache(name, key, &block) # :nodoc: + ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key), &block) end end end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 3084886ec54b9..cba63b2e38634 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -1,97 +1,128 @@ # frozen_string_literal: true +# :markup: markdown + module AbstractController - # = Abstract Controller Callbacks - # - # Abstract Controller provides hooks during the life cycle of a controller action. - # Callbacks allow you to trigger logic during this cycle. Available callbacks are: - # - # * after_action - # * append_after_action - # * append_around_action - # * append_before_action - # * around_action - # * before_action - # * prepend_after_action - # * prepend_around_action - # * prepend_before_action - # * skip_after_action - # * skip_around_action - # * skip_before_action + # # Abstract Controller Callbacks # - # NOTE: Calling the same callback multiple times will overwrite previous callback definitions. + # Abstract Controller provides hooks during the life cycle of a controller + # action. Callbacks allow you to trigger logic during this cycle. Available + # callbacks are: # + # * `after_action` + # * `append_after_action` + # * `append_around_action` + # * `append_before_action` + # * `around_action` + # * `before_action` + # * `prepend_after_action` + # * `prepend_around_action` + # * `prepend_before_action` + # * `skip_after_action` + # * `skip_around_action` + # * `skip_before_action` module Callbacks extend ActiveSupport::Concern - # Uses ActiveSupport::Callbacks as the base functionality. For - # more details on the whole callback system, read the documentation - # for ActiveSupport::Callbacks. + # Uses ActiveSupport::Callbacks as the base functionality. For more details on + # the whole callback system, read the documentation for + # ActiveSupport::Callbacks. include ActiveSupport::Callbacks included do define_callbacks :process_action, terminator: ->(controller, result_lambda) { result_lambda.call; controller.performed? }, skip_after_callbacks_if_terminated: true + mattr_accessor :raise_on_missing_callback_actions, default: false end - # Override AbstractController::Base#process_action to run the - # process_action callbacks around the normal behavior. - def process_action(*) - run_callbacks(:process_action) do - super + class ActionFilter # :nodoc: + def initialize(filters, conditional_key, actions) + @filters = filters.to_a + @conditional_key = conditional_key + @actions = Array(actions).map(&:to_s).to_set + end + + def match?(controller) + if controller.raise_on_missing_callback_actions + missing_action = @actions.find { |action| !controller.available_action?(action) } + if missing_action + filter_names = @filters.length == 1 ? @filters.first.inspect : @filters.inspect + + message = <<~MSG + The #{missing_action} action could not be found for the #{filter_names} + callback on #{controller.class.name}, but it is listed in the controller's + #{@conditional_key.inspect} option. + + Raising for missing callback actions is a new default in Rails 7.1, if you'd + like to turn this off you can delete the option from the environment configurations + or set `config.action_controller.raise_on_missing_callback_actions` to `false`. + MSG + + raise ActionNotFound.new(message, controller, missing_action) + end + end + + @actions.include?(controller.action_name) end + + alias after match? + alias before match? + alias around match? end module ClassMethods - # If +:only+ or +:except+ are used, convert the options into the - # +:if+ and +:unless+ options of ActiveSupport::Callbacks. + # If `:only` or `:except` are used, convert the options into the `:if` and + # `:unless` options of ActiveSupport::Callbacks. + # + # The basic idea is that `:only => :index` gets converted to `:if => proc {|c| + # c.action_name == "index" }`. # - # The basic idea is that :only => :index gets converted to - # :if => proc {|c| c.action_name == "index" }. + # Note that `:only` has priority over `:if` in case they are used together. # - # Note that :only has priority over :if in case they - # are used together. + # only: :index, if: -> { true } # the :if option will be ignored. # - # only: :index, if: -> { true } # the :if option will be ignored. + # Note that `:if` has priority over `:except` in case they are used together. # - # Note that :if has priority over :except in case they - # are used together. + # except: :index, if: -> { true } # the :except option will be ignored. # - # except: :index, if: -> { true } # the :except option will be ignored. + # #### Options + # * `only` - The callback should be run only for this action. + # * `except` - The callback should be run for all actions except this action. # - # ==== Options - # * only - The callback should be run only for this action. - # * except - The callback should be run for all actions except this action. def _normalize_callback_options(options) _normalize_callback_option(options, :only, :if) _normalize_callback_option(options, :except, :unless) end def _normalize_callback_option(options, from, to) # :nodoc: - if from = options.delete(from) - _from = Array(from).map(&:to_s).to_set - from = proc { |c| _from.include? c.action_name } - options[to] = Array(options[to]).unshift(from) + if from_value = options.delete(from) + filters = options[:filters] + from_value = ActionFilter.new(filters, from, from_value) + options[to] = Array(options[to]).unshift(from_value) end end - # Take callback names and an optional callback proc, normalize them, - # then call the block with each callback. This allows us to abstract - # the normalization across several methods that use it. + # Take callback names and an optional callback proc, normalize them, then call + # the block with each callback. This allows us to abstract the normalization + # across several methods that use it. + # + # #### Parameters + # * `callbacks` - An array of callbacks, with an optional options hash as the + # last parameter. + # * `block` - A proc that should be added to the callbacks. # - # ==== Parameters - # * callbacks - An array of callbacks, with an optional - # options hash as the last parameter. - # * block - A proc that should be added to the callbacks. # - # ==== Block Parameters - # * name - The callback to be added. - # * options - A hash of options to be used when adding the callback. + # #### Block Parameters + # * `name` - The callback to be added. + # * `options` - A hash of options to be used when adding the callback. + # def _insert_callbacks(callbacks, block = nil) options = callbacks.extract_options! - _normalize_callback_options(options) callbacks.push(block) if block + options[:filters] = callbacks + _normalize_callback_options(options) + options.delete(:filters) callbacks.each do |callback| yield callback, options end @@ -104,20 +135,21 @@ def _insert_callbacks(callbacks, block = nil) # # Append a callback before actions. See _insert_callbacks for parameter details. # - # If the callback renders or redirects, the action will not run. If there - # are additional callbacks scheduled to run after that callback, they are - # also cancelled. + # If the callback renders or redirects, the action will not run. If there are + # additional callbacks scheduled to run after that callback, they are also + # cancelled. ## # :method: prepend_before_action # # :call-seq: prepend_before_action(names, block) # - # Prepend a callback before actions. See _insert_callbacks for parameter details. + # Prepend a callback before actions. See _insert_callbacks for parameter + # details. # - # If the callback renders or redirects, the action will not run. If there - # are additional callbacks scheduled to run after that callback, they are - # also cancelled. + # If the callback renders or redirects, the action will not run. If there are + # additional callbacks scheduled to run after that callback, they are also + # cancelled. ## # :method: skip_before_action @@ -133,9 +165,9 @@ def _insert_callbacks(callbacks, block = nil) # # Append a callback before actions. See _insert_callbacks for parameter details. # - # If the callback renders or redirects, the action will not run. If there - # are additional callbacks scheduled to run after that callback, they are - # also cancelled. + # If the callback renders or redirects, the action will not run. If there are + # additional callbacks scheduled to run after that callback, they are also + # cancelled. ## # :method: after_action @@ -177,7 +209,8 @@ def _insert_callbacks(callbacks, block = nil) # # :call-seq: prepend_around_action(names, block) # - # Prepend a callback around actions. See _insert_callbacks for parameter details. + # Prepend a callback around actions. See _insert_callbacks for parameter + # details. ## # :method: skip_around_action @@ -192,9 +225,8 @@ def _insert_callbacks(callbacks, block = nil) # :call-seq: append_around_action(names, block) # # Append a callback around actions. See _insert_callbacks for parameter details. - - # set up before_action, prepend_before_action, skip_before_action, etc. - # for each of before, after, and around. + # set up before_action, prepend_before_action, skip_before_action, etc. for each + # of before, after, and around. [:before, :after, :around].each do |callback| define_method "#{callback}_action" do |*names, &blk| _insert_callbacks(names, blk) do |name, options| @@ -208,8 +240,8 @@ def _insert_callbacks(callbacks, block = nil) end end - # Skip a before, after or around callback. See _insert_callbacks - # for details on the allowed parameters. + # Skip a before, after or around callback. See _insert_callbacks for details on + # the allowed parameters. define_method "skip_#{callback}_action" do |*names| _insert_callbacks(names) do |name, options| skip_callback(:process_action, callback, name, options) @@ -220,5 +252,14 @@ def _insert_callbacks(callbacks, block = nil) alias_method :"append_#{callback}_action", :"#{callback}_action" end end + + private + # Override `AbstractController::Base#process_action` to run the `process_action` + # callbacks around the normal behavior. + def process_action(...) + run_callbacks(:process_action) do + super + end + end end end diff --git a/actionpack/lib/abstract_controller/collector.rb b/actionpack/lib/abstract_controller/collector.rb index 0af546cc961fe..dee0b49708bd5 100644 --- a/actionpack/lib/abstract_controller/collector.rb +++ b/actionpack/lib/abstract_controller/collector.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + require "action_dispatch/http/mime_type" module AbstractController @@ -7,8 +9,8 @@ module Collector def self.generate_method_for_mime(mime) sym = mime.is_a?(Symbol) ? mime : mime.to_sym class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{sym}(*args, &block) - custom(Mime[:#{sym}], *args, &block) + def #{sym}(...) + custom(Mime[:#{sym}], ...) end RUBY end @@ -22,7 +24,7 @@ def #{sym}(*args, &block) end private - def method_missing(symbol, &block) + def method_missing(symbol, ...) unless mime_constant = Mime[symbol] raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \ "https://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \ @@ -33,7 +35,7 @@ def method_missing(symbol, &block) if Mime::SET.include?(mime_constant) AbstractController::Collector.generate_method_for_mime(mime_constant) - send(symbol, &block) + public_send(symbol, ...) else super end diff --git a/actionpack/lib/abstract_controller/deprecator.rb b/actionpack/lib/abstract_controller/deprecator.rb new file mode 100644 index 0000000000000..989500fa136d3 --- /dev/null +++ b/actionpack/lib/abstract_controller/deprecator.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# :markup: markdown + +module AbstractController + def self.deprecator # :nodoc: + @deprecator ||= ActiveSupport::Deprecation.new + end +end diff --git a/actionpack/lib/abstract_controller/error.rb b/actionpack/lib/abstract_controller/error.rb index 89a54f072e082..812241bfdd0d1 100644 --- a/actionpack/lib/abstract_controller/error.rb +++ b/actionpack/lib/abstract_controller/error.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true +# :markup: markdown + module AbstractController - class Error < StandardError #:nodoc: + class Error < StandardError # :nodoc: end end diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index c977ce2039407..8be59d05ddf6e 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -1,62 +1,130 @@ # frozen_string_literal: true +# :markup: markdown + require "active_support/dependencies" +require "active_support/core_ext/name_error" module AbstractController module Helpers extend ActiveSupport::Concern included do - class_attribute :_helpers, default: define_helpers_module(self) class_attribute :_helper_methods, default: Array.new + + # This is here so that it is always higher in the inheritance chain than the + # definition in lib/action_view/rendering.rb + redefine_singleton_method(:_helpers) do + if @_helpers ||= nil + @_helpers + else + superclass._helpers + end + end + + self._helpers = define_helpers_module(self) end - class MissingHelperError < LoadError - def initialize(error, path) - @error = error - @path = "helpers/#{path}.rb" - set_backtrace error.backtrace + def _helpers + self.class._helpers + end - if /^#{path}(\.rb)?$/.match?(error.path) - super("Missing helper file helpers/%s.rb" % path) - else - raise error + module Resolution # :nodoc: + def modules_for_helpers(modules_or_helper_prefixes) + modules_or_helper_prefixes.flatten.map! do |module_or_helper_prefix| + case module_or_helper_prefix + when Module + module_or_helper_prefix + when String, Symbol + helper_prefix = module_or_helper_prefix.to_s + helper_prefix = helper_prefix.camelize unless helper_prefix.start_with?(/[A-Z]/) + "#{helper_prefix}Helper".constantize + else + raise ArgumentError, "helper must be a String, Symbol, or Module" + end end end + + def all_helpers_from_path(path) + helpers = Array(path).flat_map do |_path| + names = Dir["#{_path}/**/*_helper.rb"].map { |file| file[_path.to_s.size + 1..-"_helper.rb".size - 1] } + names.sort! + end + helpers.uniq! + helpers + end + + def helper_modules_from_paths(paths) + modules_for_helpers(all_helpers_from_path(paths)) + end end + extend Resolution + module ClassMethods - # When a class is inherited, wrap its helper module in a new module. - # This ensures that the parent class's module can be changed - # independently of the child class's. + # When a class is inherited, wrap its helper module in a new module. This + # ensures that the parent class's module can be changed independently of the + # child class's. def inherited(klass) - helpers = _helpers - klass._helpers = define_helpers_module(klass, helpers) + # Inherited from parent by default + klass._helpers = nil + klass.class_eval { default_helper_module! } unless klass.anonymous? super end + attr_writer :_helpers + + include Resolution + + ## + # :method: modules_for_helpers + # :call-seq: modules_for_helpers(modules_or_helper_prefixes) + # + # Given an array of values like the ones accepted by `helper`, this method + # returns an array with the corresponding modules, in the same order. + # + # ActionController::Base.modules_for_helpers(["application", "chart", "rubygems"]) + # # => [ApplicationHelper, ChartHelper, RubygemsHelper] + # + #-- + # Implemented by Resolution#modules_for_helpers. + + # :method: # all_helpers_from_path + # :call-seq: all_helpers_from_path(path) + # + # Returns a list of helper names in a given path. + # + # ActionController::Base.all_helpers_from_path 'app/helpers' + # # => ["application", "chart", "rubygems"] + # + #-- + # Implemented by Resolution#all_helpers_from_path. + # Declare a controller method as a helper. For example, the following - # makes the +current_user+ and +logged_in?+ controller methods available + # makes the `current_user` and `logged_in?` controller methods available # to the view: - # class ApplicationController < ActionController::Base - # helper_method :current_user, :logged_in? # - # def current_user - # @current_user ||= User.find_by(id: session[:user]) - # end + # class ApplicationController < ActionController::Base + # helper_method :current_user, :logged_in? # - # def logged_in? - # current_user != nil + # private + # def current_user + # @current_user ||= User.find_by(id: session[:user]) + # end + # + # def logged_in? + # current_user != nil + # end # end - # end # # In a view: - # <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%> # - # ==== Parameters - # * method[, method] - A name or names of a method on the controller - # to be made available on the view. + # <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%> + # + # #### Parameters + # * `method[, method]` - A name or names of a method on the controller to be + # made available on the view. def helper_method(*methods) methods.flatten! self._helper_methods += methods @@ -65,59 +133,79 @@ def helper_method(*methods) file, line = location.path, location.lineno methods.each do |method| - _helpers.class_eval <<-ruby_eval, file, line - def #{method}(*args, &block) # def current_user(*args, &block) - controller.send(:'#{method}', *args, &block) # controller.send(:'current_user', *args, &block) - end # end - ruby2_keywords(:'#{method}') if respond_to?(:ruby2_keywords, true) + # def current_user(...) + # controller.send(:'current_user', ...) + # end + _helpers_for_modification.class_eval <<~ruby_eval.lines.map(&:strip).join(";"), file, line + def #{method}(...) + controller.send(:'#{method}', ...) + end ruby_eval end end - # The +helper+ class method can take a series of helper module names, a block, or both. + # Includes the given modules in the template class. # - # ==== Options - # * *args - Module, Symbol, String - # * block - A block defining helper methods + # Modules can be specified in different ways. All of the following calls include + # `FooHelper`: # - # When the argument is a module it will be included directly in the template class. - # helper FooHelper # => includes FooHelper + # # Module, recommended. + # helper FooHelper # - # When the argument is a string or symbol, the method will provide the "_helper" suffix, require the file - # and include the module in the template class. The second form illustrates how to include custom helpers - # when working with namespaced controllers, or other cases where the file containing the helper definition is not - # in one of Rails' standard load paths: - # helper :foo # => requires 'foo_helper' and includes FooHelper - # helper 'resources/foo' # => requires 'resources/foo_helper' and includes Resources::FooHelper + # # String/symbol without the "helper" suffix, camel or snake case. + # helper "Foo" + # helper :Foo + # helper "foo" + # helper :foo # - # Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available - # to the template. + # The last two assume that `"foo".camelize` returns "Foo". # - # # One line - # helper { def hello() "Hello, world!" end } + # When strings or symbols are passed, the method finds the actual module object + # using String#constantize. Therefore, if the module has not been yet loaded, it + # has to be autoloadable, which is normally the case. # - # # Multi-line - # helper do - # def foo(bar) - # "#{bar} is the very best" + # Namespaces are supported. The following calls include `Foo::BarHelper`: + # + # # Module, recommended. + # helper Foo::BarHelper + # + # # String/symbol without the "helper" suffix, camel or snake case. + # helper "Foo::Bar" + # helper :"Foo::Bar" + # helper "foo/bar" + # helper :"foo/bar" + # + # The last two assume that `"foo/bar".camelize` returns "Foo::Bar". + # + # The method accepts a block too. If present, the block is evaluated in the + # context of the controller helper module. This simple call makes the `wadus` + # method available in templates of the enclosing controller: + # + # helper do + # def wadus + # "wadus" + # end # end - # end # - # Finally, all the above styles can be mixed together, and the +helper+ method can be invoked with a mix of - # +symbols+, +strings+, +modules+ and blocks. + # Furthermore, all the above styles can be mixed together: # - # helper(:three, BlindHelper) { def mice() 'mice' end } + # helper FooHelper, "woo", "bar/baz" do + # def wadus + # "wadus" + # end + # end # def helper(*args, &block) modules_for_helpers(args).each do |mod| - _helpers.include(mod) + next if _helpers.include?(mod) + _helpers_for_modification.include(mod) end - _helpers.module_eval(&block) if block_given? + _helpers_for_modification.module_eval(&block) if block_given? end - # Clears up all existing helpers in this class, only keeping the helper - # with the same name as this class. + # Clears up all existing helpers in this class, only keeping the helper with the + # same name as this class. def clear_helpers inherited_helper_methods = _helper_methods self._helpers = Module.new @@ -127,56 +215,17 @@ def clear_helpers default_helper_module! unless anonymous? end - # Returns a list of modules, normalized from the acceptable kinds of - # helpers with the following behavior: - # - # String or Symbol:: :FooBar or "FooBar" becomes "foo_bar_helper", - # and "foo_bar_helper.rb" is loaded using require_dependency. - # - # Module:: No further processing - # - # After loading the appropriate files, the corresponding modules - # are returned. - # - # ==== Parameters - # * args - An array of helpers - # - # ==== Returns - # * Array - A normalized list of modules for the list of - # helpers provided. - def modules_for_helpers(args) - args.flatten.map! do |arg| - case arg - when String, Symbol - file_name = "#{arg.to_s.underscore}_helper" - begin - require_dependency(file_name) - rescue LoadError => e - raise AbstractController::Helpers::MissingHelperError.new(e, file_name) - end - - mod_name = file_name.camelize - begin - mod_name.constantize - rescue LoadError - # dependencies.rb gives a similar error message but its wording is - # not as clear because it mentions autoloading. To the user all it - # matters is that a helper module couldn't be loaded, autoloading - # is an internal mechanism that should not leak. - raise NameError, "Couldn't find #{mod_name}, expected it to be defined in helpers/#{file_name}.rb" - end - when Module - arg - else - raise ArgumentError, "helper must be a String, Symbol, or Module" - end + def _helpers_for_modification + unless @_helpers + self._helpers = define_helpers_module(self, superclass._helpers) end + _helpers end private def define_helpers_module(klass, helpers = nil) - # In some tests inherited is called explicitly. In that case, just - # return the module from the first time it was defined + # In some tests inherited is called explicitly. In that case, just return the + # module from the first time it was defined return klass.const_get(:HelperMethods) if klass.const_defined?(:HelperMethods, false) mod = Module.new @@ -186,13 +235,10 @@ def define_helpers_module(klass, helpers = nil) end def default_helper_module! - module_name = name.sub(/Controller$/, "") - module_path = module_name.underscore - helper module_path - rescue LoadError => e - raise e unless e.is_missing? "helpers/#{module_path}_helper" + helper_prefix = name.delete_suffix("Controller") + helper(helper_prefix) rescue NameError => e - raise e unless e.missing_name? "#{module_name}Helper" + raise unless e.missing_name?("#{helper_prefix}Helper") end end end diff --git a/actionpack/lib/abstract_controller/logger.rb b/actionpack/lib/abstract_controller/logger.rb index 8d0acc1b5ca08..5a81638dd69ea 100644 --- a/actionpack/lib/abstract_controller/logger.rb +++ b/actionpack/lib/abstract_controller/logger.rb @@ -1,13 +1,16 @@ # frozen_string_literal: true +# :markup: markdown + require "active_support/benchmarkable" module AbstractController - module Logger #:nodoc: + module Logger # :nodoc: extend ActiveSupport::Concern included do - config_accessor :logger + singleton_class.delegate :logger, :logger=, to: :config + delegate :logger, :logger=, to: :config include ActiveSupport::Benchmarkable end end diff --git a/actionpack/lib/abstract_controller/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb index fbd93705edee3..3e909512e41fc 100644 --- a/actionpack/lib/abstract_controller/railties/routes_helpers.rb +++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +# :markup: markdown + +require "active_support/core_ext/module/introspection" + module AbstractController module Railties module RoutesHelpers @@ -7,6 +11,7 @@ def self.with(routes, include_path_helpers = true) Module.new do define_method(:inherited) do |klass| super(klass) + if namespace = klass.module_parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) } klass.include(namespace.railtie_routes_url_helpers(include_path_helpers)) else diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 72629a0995923..346bb4e2b9102 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true +# :markup: markdown + require "abstract_controller/error" require "action_view" require "action_view/view_paths" -require "set" module AbstractController class DoubleRenderError < Error - DEFAULT_MESSAGE = "Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like \"redirect_to(...) and return\"." + DEFAULT_MESSAGE = "Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like \"redirect_to(...); return\"." def initialize(message = nil) super(message || DEFAULT_MESSAGE) @@ -18,8 +19,10 @@ module Rendering extend ActiveSupport::Concern include ActionView::ViewPaths - # Normalizes arguments, options and then delegates render_to_body and - # sticks the result in self.response_body. + # Normalizes arguments and options, and then delegates to render_to_body and + # sticks the result in `self.response_body`. + # + # Supported options depend on the underlying `render_to_body` implementation. def render(*args, &block) options = _normalize_render(*args, &block) rendered_body = render_to_body(options) @@ -32,16 +35,12 @@ def render(*args, &block) self.response_body = rendered_body end - # Raw rendering of a template to a string. - # - # It is similar to render, except that it does not - # set the +response_body+ and it should be guaranteed - # to always return a string. + # Similar to #render, but only returns the rendered template as a string, + # instead of setting `self.response_body`. # - # If a component extends the semantics of +response_body+ - # (as ActionController extends it to be anything that - # responds to the method each), this method needs to be - # overridden in order to still return a string. + # If a component extends the semantics of `response_body` (as ActionController + # extends it to be anything that responds to the method each), this method needs + # to be overridden in order to still return a string. def render_to_string(*args, &block) options = _normalize_render(*args, &block) render_to_body(options) @@ -51,15 +50,15 @@ def render_to_string(*args, &block) def render_to_body(options = {}) end - # Returns Content-Type of rendered content. + # Returns `Content-Type` of rendered content. def rendered_format Mime[:text] end DEFAULT_PROTECTED_INSTANCE_VARIABLES = %i(@_action_name @_response_body @_formats @_prefixes) - # This method should return a hash with assigns. - # You can overwrite this configuration per controller. + # This method should return a hash with assigns. You can overwrite this + # configuration per controller. def view_assigns variables = instance_variables - _protected_ivars @@ -69,9 +68,8 @@ def view_assigns end private - # Normalize args by converting render "foo" to - # render :action => "foo" and render "foo/bar" to - # render :file => "foo/bar". + # Normalize args by converting `render "foo"` to `render action: "foo"` and + # `render "foo/bar"` to `render file: "foo/bar"`. def _normalize_args(action = nil, options = {}) # :doc: if action.respond_to?(:permitted?) if action.permitted? diff --git a/actionpack/lib/abstract_controller/translation.rb b/actionpack/lib/abstract_controller/translation.rb index 4c0a29156f9ba..212190471e63e 100644 --- a/actionpack/lib/abstract_controller/translation.rb +++ b/actionpack/lib/abstract_controller/translation.rb @@ -1,28 +1,39 @@ # frozen_string_literal: true +# :markup: markdown + +require "active_support/html_safe_translation" + module AbstractController module Translation - # Delegates to I18n.translate. Also aliased as t. + # Delegates to `I18n.translate`. # # When the given key starts with a period, it will be scoped by the current - # controller and action. So if you call translate(".foo") from - # PeopleController#index, it will convert the call to - # I18n.translate("people.index.foo"). This makes it less repetitive - # to translate many keys within the same controller / action and gives you a - # simple framework for scoping them consistently. + # controller and action. So if you call `translate(".foo")` from + # `PeopleController#index`, it will convert the call to + # `I18n.translate("people.index.foo")`. This makes it less repetitive to + # translate many keys within the same controller / action and gives you a simple + # framework for scoping them consistently. def translate(key, **options) - if key.to_s.start_with?(".") + if key&.start_with?(".") path = controller_path.tr("/", ".") defaults = [:"#{path}#{key}"] defaults << options[:default] if options[:default] options[:default] = defaults.flatten key = "#{path}.#{action_name}#{key}" end - I18n.translate(key, **options) + + if options[:default] && ActiveSupport::HtmlSafeTranslation.html_safe_translation_key?(key) + options[:default] = Array(options[:default]).map do |value| + value.is_a?(String) ? ERB::Util.html_escape(value) : value + end + end + + ActiveSupport::HtmlSafeTranslation.translate(key, **options) end alias :t :translate - # Delegates to I18n.localize. Also aliased as l. + # Delegates to `I18n.localize`. def localize(object, **options) I18n.localize(object, **options) end diff --git a/actionpack/lib/abstract_controller/url_for.rb b/actionpack/lib/abstract_controller/url_for.rb index bd74c27d3b82a..c2d2da709981d 100644 --- a/actionpack/lib/abstract_controller/url_for.rb +++ b/actionpack/lib/abstract_controller/url_for.rb @@ -1,12 +1,16 @@ # frozen_string_literal: true +# :markup: markdown + module AbstractController - # Includes +url_for+ into the host class (e.g. an abstract controller or mailer). The class - # has to provide a +RouteSet+ by implementing the _routes methods. Otherwise, an - # exception will be raised. + # # URL For + # + # Includes `url_for` into the host class (e.g. an abstract controller or + # mailer). The class has to provide a `RouteSet` by implementing the `_routes` + # methods. Otherwise, an exception will be raised. # - # Note that this module is completely decoupled from HTTP - the only requirement is a valid - # _routes implementation. + # Note that this module is completely decoupled from HTTP - the only requirement + # is a valid `_routes` implementation. module UrlFor extend ActiveSupport::Concern include ActionDispatch::Routing::UrlFor @@ -22,12 +26,10 @@ def _routes end def action_methods - @action_methods ||= begin - if _routes - super - _routes.named_routes.helper_names - else - super - end + @action_methods ||= if _routes + super - _routes.named_routes.helper_names + else + super end end end diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 0187b133c4be2..847199aaefff0 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -1,16 +1,26 @@ # frozen_string_literal: true +# :markup: markdown + require "abstract_controller" require "action_dispatch" +require "action_controller/deprecator" require "action_controller/metal/strong_parameters" +require "action_controller/metal/exceptions" +# # Action Controller +# +# Action Controller is a module of Action Pack. +# +# Action Controller provides a base controller class that can be subclassed to +# implement filters and actions to handle requests. The result of an action is +# typically content generated from views. module ActionController extend ActiveSupport::Autoload autoload :API autoload :Base autoload :Metal - autoload :Middleware autoload :Renderer autoload :FormBuilder @@ -19,10 +29,7 @@ module ActionController end autoload_under "metal" do - eager_autoload do - autoload :Live - end - + autoload :AllowBrowser autoload :ConditionalGet autoload :ContentSecurityPolicy autoload :Cookies @@ -30,18 +37,19 @@ module ActionController autoload :DefaultHeaders autoload :EtagWithTemplateDigest autoload :EtagWithFlash - autoload :FeaturePolicy + autoload :PermissionsPolicy autoload :Flash - autoload :ForceSSL autoload :Head autoload :Helpers autoload :HttpAuthentication autoload :BasicImplicitRender autoload :ImplicitRender autoload :Instrumentation + autoload :Live autoload :Logging autoload :MimeResponds autoload :ParamsWrapper + autoload :RateLimiting autoload :Redirecting autoload :Renderers autoload :Rendering @@ -58,14 +66,15 @@ module ActionController autoload :ApiRendering end - autoload :TestCase, "action_controller/test_case" - autoload :TemplateAssertions, "action_controller/test_case" + autoload_at "action_controller/test_case" do + autoload :TestCase + autoload :TestRequest + autoload :TemplateAssertions + end end # Common Active Support usage in Action Controller require "active_support/core_ext/module/attribute_accessors" -require "active_support/core_ext/load_error" require "active_support/core_ext/module/attr_internal" require "active_support/core_ext/name_error" -require "active_support/core_ext/uri" require "active_support/inflector" diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb index 6ad89cf611408..b52430467b13f 100644 --- a/actionpack/lib/action_controller/api.rb +++ b/actionpack/lib/action_controller/api.rb @@ -1,105 +1,108 @@ # frozen_string_literal: true +# :markup: markdown + require "action_view" require "action_controller" require "action_controller/log_subscriber" module ActionController - # API Controller is a lightweight version of ActionController::Base, - # created for applications that don't require all functionalities that a complete - # \Rails controller provides, allowing you to create controllers with just the - # features that you need for API only applications. - # - # An API Controller is different from a normal controller in the sense that - # by default it doesn't include a number of features that are usually required - # by browser access only: layouts and templates rendering, - # flash, assets, and so on. This makes the entire controller stack thinner, - # suitable for API applications. It doesn't mean you won't have such - # features if you need them: they're all available for you to include in - # your application, they're just not part of the default API controller stack. - # - # Normally, +ApplicationController+ is the only controller that inherits from - # ActionController::API. All other controllers in turn inherit from - # +ApplicationController+. + # # Action Controller API + # + # API Controller is a lightweight version of ActionController::Base, created for + # applications that don't require all functionalities that a complete Rails + # controller provides, allowing you to create controllers with just the features + # that you need for API only applications. + # + # An API Controller is different from a normal controller in the sense that by + # default it doesn't include a number of features that are usually required by + # browser access only: layouts and templates rendering, flash, assets, and so + # on. This makes the entire controller stack thinner, suitable for API + # applications. It doesn't mean you won't have such features if you need them: + # they're all available for you to include in your application, they're just not + # part of the default API controller stack. + # + # Normally, `ApplicationController` is the only controller that inherits from + # `ActionController::API`. All other controllers in turn inherit from + # `ApplicationController`. # # A sample controller could look like this: # - # class PostsController < ApplicationController - # def index - # posts = Post.all - # render json: posts + # class PostsController < ApplicationController + # def index + # posts = Post.all + # render json: posts + # end # end - # end # # Request, response, and parameters objects all work the exact same way as - # ActionController::Base. + # ActionController::Base. # - # == Renders + # ## Renders # - # The default API Controller stack includes all renderers, which means you - # can use render :json and brothers freely in your controllers. Keep - # in mind that templates are not going to be rendered, so you need to ensure - # your controller is calling either render or redirect_to in - # all actions, otherwise it will return 204 No Content. + # The default API Controller stack includes all renderers, which means you can + # use `render :json` and siblings freely in your controllers. Keep in mind that + # templates are not going to be rendered, so you need to ensure your controller + # is calling either `render` or `redirect_to` in all actions, otherwise it will + # return `204 No Content`. # - # def show - # post = Post.find(params[:id]) - # render json: post - # end + # def show + # post = Post.find(params[:id]) + # render json: post + # end # - # == Redirects + # ## Redirects # # Redirects are used to move from one action to another. You can use the - # redirect_to method in your controllers in the same way as in - # ActionController::Base. For example: + # `redirect_to` method in your controllers in the same way as in + # ActionController::Base. For example: # - # def create - # redirect_to root_url and return if not_authorized? - # # do stuff here - # end + # def create + # redirect_to root_url and return if not_authorized? + # # do stuff here + # end # - # == Adding New Behavior + # ## Adding New Behavior # # In some scenarios you may want to add back some functionality provided by - # ActionController::Base that is not present by default in - # ActionController::API, for instance MimeResponds. This - # module gives you the respond_to method. Adding it is quite simple, - # you just need to include the module in a specific controller or in - # +ApplicationController+ in case you want it available in your entire - # application: - # - # class ApplicationController < ActionController::API - # include ActionController::MimeResponds - # end - # - # class PostsController < ApplicationController - # def index - # posts = Post.all - # - # respond_to do |format| - # format.json { render json: posts } - # format.xml { render xml: posts } + # ActionController::Base that is not present by default in + # `ActionController::API`, for instance `MimeResponds`. This module gives you + # the `respond_to` method. Adding it is quite simple, you just need to include + # the module in a specific controller or in `ApplicationController` in case you + # want it available in your entire application: + # + # class ApplicationController < ActionController::API + # include ActionController::MimeResponds + # end + # + # class PostsController < ApplicationController + # def index + # posts = Post.all + # + # respond_to do |format| + # format.json { render json: posts } + # format.xml { render xml: posts } + # end # end # end - # end # - # Make sure to check the modules included in ActionController::Base - # if you want to use any other functionality that is not provided - # by ActionController::API out of the box. + # Make sure to check the modules included in ActionController::Base if you want + # to use any other functionality that is not provided by `ActionController::API` + # out of the box. class API < Metal abstract! - # Shortcut helper that returns all the ActionController::API modules except - # the ones passed as arguments: + # Shortcut helper that returns all the ActionController::API modules except the + # ones passed as arguments: # - # class MyAPIBaseController < ActionController::Metal - # ActionController::API.without_modules(:ForceSSL, :UrlFor).each do |left| - # include left + # class MyAPIBaseController < ActionController::Metal + # ActionController::API.without_modules(:UrlFor).each do |left| + # include left + # end # end - # end # - # This gives better control over what you want to exclude and makes it easier - # to create an API controller class, instead of listing the modules required + # This gives better control over what you want to exclude and makes it easier to + # create an API controller class, instead of listing the modules required # manually. def self.without_modules(*modules) modules = modules.map do |m| @@ -119,25 +122,26 @@ def self.without_modules(*modules) ConditionalGet, BasicImplicitRender, StrongParameters, + RateLimiting, + Caching, - ForceSSL, DataStreaming, DefaultHeaders, Logging, - # Before callbacks should also be executed as early as possible, so - # also include them at the bottom. + # Before callbacks should also be executed as early as possible, so also include + # them at the bottom. AbstractController::Callbacks, # Append rescue at the bottom to wrap as much as possible. Rescue, - # Add instrumentations hooks at the bottom, to ensure they instrument - # all the methods properly. + # Add instrumentations hooks at the bottom, to ensure they instrument all the + # methods properly. Instrumentation, - # Params wrapper should come before instrumentation so they are - # properly showed in logs + # Params wrapper should come before instrumentation so they are properly showed + # in logs ParamsWrapper ] diff --git a/actionpack/lib/action_controller/api/api_rendering.rb b/actionpack/lib/action_controller/api/api_rendering.rb index aca526531344d..dcb27315f1606 100644 --- a/actionpack/lib/action_controller/api/api_rendering.rb +++ b/actionpack/lib/action_controller/api/api_rendering.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionController module ApiRendering extend ActiveSupport::Concern diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 5fa50985848f1..279dbdd71ecb2 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -1,199 +1,224 @@ # frozen_string_literal: true +# :markup: markdown + require "action_view" require "action_controller/log_subscriber" require "action_controller/metal/params_wrapper" module ActionController - # Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed - # on request and then either it renders a template or redirects to another action. An action is defined as a public method - # on the controller, which will automatically be made accessible to the web-server through \Rails Routes. + # # Action Controller Base + # + # Action Controllers are the core of a web request in Rails. They are made up of + # one or more actions that are executed on request and then either it renders a + # template or redirects to another action. An action is defined as a public + # method on the controller, which will automatically be made accessible to the + # web-server through Rails Routes. # - # By default, only the ApplicationController in a \Rails application inherits from ActionController::Base. All other - # controllers inherit from ApplicationController. This gives you one class to configure things such as + # By default, only the ApplicationController in a Rails application inherits + # from `ActionController::Base`. All other controllers inherit from + # ApplicationController. This gives you one class to configure things such as # request forgery protection and filtering of sensitive request parameters. # # A sample controller could look like this: # - # class PostsController < ApplicationController - # def index - # @posts = Post.all - # end + # class PostsController < ApplicationController + # def index + # @posts = Post.all + # end # - # def create - # @post = Post.create params[:post] - # redirect_to posts_path + # def create + # @post = Post.create params[:post] + # redirect_to posts_path + # end # end - # end # - # Actions, by default, render a template in the app/views directory corresponding to the name of the controller and action - # after executing code in the action. For example, the +index+ action of the PostsController would render the - # template app/views/posts/index.html.erb by default after populating the @posts instance variable. + # Actions, by default, render a template in the `app/views` directory + # corresponding to the name of the controller and action after executing code in + # the action. For example, the `index` action of the PostsController would + # render the template `app/views/posts/index.html.erb` by default after + # populating the `@posts` instance variable. # - # Unlike index, the create action will not render a template. After performing its main purpose (creating a - # new post), it initiates a redirect instead. This redirect works by returning an external - # 302 Moved HTTP response that takes the user to the index action. + # Unlike index, the create action will not render a template. After performing + # its main purpose (creating a new post), it initiates a redirect instead. This + # redirect works by returning an external `302 Moved` HTTP response that takes + # the user to the index action. # - # These two methods represent the two basic action archetypes used in Action Controllers: Get-and-show and do-and-redirect. - # Most actions are variations on these themes. + # These two methods represent the two basic action archetypes used in Action + # Controllers: Get-and-show and do-and-redirect. Most actions are variations on + # these themes. # - # == Requests + # ## Requests # - # For every request, the router determines the value of the +controller+ and +action+ keys. These determine which controller - # and action are called. The remaining request parameters, the session (if one is available), and the full request with - # all the HTTP headers are made available to the action through accessor methods. Then the action is performed. + # For every request, the router determines the value of the `controller` and + # `action` keys. These determine which controller and action are called. The + # remaining request parameters, the session (if one is available), and the full + # request with all the HTTP headers are made available to the action through + # accessor methods. Then the action is performed. # - # The full request object is available via the request accessor and is primarily used to query for HTTP headers: + # The full request object is available via the request accessor and is primarily + # used to query for HTTP headers: # - # def server_ip - # location = request.env["REMOTE_ADDR"] - # render plain: "This server hosted at #{location}" - # end + # def server_ip + # location = request.env["REMOTE_ADDR"] + # render plain: "This server hosted at #{location}" + # end # - # == Parameters + # ## Parameters # - # All request parameters, whether they come from a query string in the URL or form data submitted through a POST request are - # available through the params method which returns a hash. For example, an action that was performed through - # /posts?category=All&limit=5 will include { "category" => "All", "limit" => "5" } in params. + # All request parameters, whether they come from a query string in the URL or + # form data submitted through a POST request are available through the `params` + # method which returns a hash. For example, an action that was performed through + # `/posts?category=All&limit=5` will include `{ "category" => "All", "limit" => + # "5" }` in `params`. # - # It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as: + # It's also possible to construct multi-dimensional parameter hashes by + # specifying keys using brackets, such as: # - # - # + # + # # - # A request coming from a form holding these inputs will include { "post" => { "name" => "david", "address" => "hyacintvej" } }. - # If the address input had been named post[address][street], the params would have included - # { "post" => { "address" => { "street" => "hyacintvej" } } }. There's no limit to the depth of the nesting. + # A request coming from a form holding these inputs will include `{ "post" => { + # "name" => "david", "address" => "hyacintvej" } }`. If the address input had + # been named `post[address][street]`, the `params` would have included `{ "post" + # => { "address" => { "street" => "hyacintvej" } } }`. There's no limit to the + # depth of the nesting. # - # == Sessions + # ## Sessions # - # Sessions allow you to store objects in between requests. This is useful for objects that are not yet ready to be persisted, - # such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such - # as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely - # they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at. + # Sessions allow you to store objects in between requests. This is useful for + # objects that are not yet ready to be persisted, such as a Signup object + # constructed in a multi-paged process, or objects that don't change much and + # are needed all the time, such as a User object for a system that requires + # login. The session should not be used, however, as a cache for objects where + # it's likely they could be changed unknowingly. It's usually too much work to + # keep it all synchronized -- something databases already excel at. # - # You can place objects in the session by using the session method, which accesses a hash: + # You can place objects in the session by using the `session` method, which + # accesses a hash: # - # session[:person] = Person.authenticate(user_name, password) + # session[:person] = Person.authenticate(user_name, password) # # You can retrieve it again through the same hash: # - # "Hello #{session[:person]}" - # - # For removing objects from the session, you can either assign a single key to +nil+: + # "Hello #{session[:person]}" # - # # removes :person from session - # session[:person] = nil + # For removing objects from the session, you can either assign a single key to + # `nil`: # - # or you can remove the entire session with +reset_session+. + # # removes :person from session + # session[:person] = nil # - # Sessions are stored by default in a browser cookie that's cryptographically signed, but unencrypted. - # This prevents the user from tampering with the session but also allows them to see its contents. + # or you can remove the entire session with `reset_session`. # - # Do not put secret information in cookie-based sessions! + # By default, sessions are stored in an encrypted browser cookie (see + # ActionDispatch::Session::CookieStore). Thus the user will not be able to read + # or edit the session data. However, the user can keep a copy of the cookie even + # after it has expired, so you should avoid storing sensitive information in + # cookie-based sessions. # - # == Responses + # ## Responses # - # Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response - # object is generated automatically through the use of renders and redirects and requires no user intervention. + # Each action results in a response, which holds the headers and document to be + # sent to the user's browser. The actual response object is generated + # automatically through the use of renders and redirects and requires no user + # intervention. # - # == Renders + # ## Renders # - # Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering - # of a template. Included in the Action Pack is the Action View, which enables rendering of ERB templates. It's automatically configured. - # The controller passes objects to the view by assigning instance variables: + # Action Controller sends content to the user by using one of five rendering + # methods. The most versatile and common is the rendering of a template. + # Also included with \Rails is Action View, which enables rendering of ERB + # templates. It's automatically configured. The controller passes objects to the + # view by assigning instance variables: # - # def show - # @post = Post.find(params[:id]) - # end + # def show + # @post = Post.find(params[:id]) + # end # # Which are then automatically available to the view: # - # Title: <%= @post.title %> + # Title: <%= @post.title %> # - # You don't have to rely on the automated rendering. For example, actions that could result in the rendering of different templates - # will use the manual rendering methods: + # You don't have to rely on the automated rendering. For example, actions that + # could result in the rendering of different templates will use the manual + # rendering methods: # - # def search - # @results = Search.find(params[:query]) - # case @results.count - # when 0 then render action: "no_results" - # when 1 then render action: "show" - # when 2..10 then render action: "show_many" + # def search + # @results = Search.find(params[:query]) + # case @results.count + # when 0 then render action: "no_results" + # when 1 then render action: "show" + # when 2..10 then render action: "show_many" + # end # end - # end # # Read more about writing ERB and Builder templates in ActionView::Base. # - # == Redirects + # ## Redirects # - # Redirects are used to move from one action to another. For example, after a create action, which stores a blog entry to the - # database, we might like to show the user the new entry. Because we're following good DRY principles (Don't Repeat Yourself), we're - # going to reuse (and redirect to) a show action that we'll assume has already been created. The code might look like this: + # Redirects are used to move from one action to another. For example, after a + # `create` action, which stores a blog entry to the database, we might like to + # show the user the new entry. Because we're following good DRY principles + # (Don't Repeat Yourself), we're going to reuse (and redirect to) a `show` + # action that we'll assume has already been created. The code might look like + # this: # - # def create - # @entry = Entry.new(params[:entry]) - # if @entry.save - # # The entry was saved correctly, redirect to show - # redirect_to action: 'show', id: @entry.id - # else - # # things didn't go so well, do something else + # def create + # @entry = Entry.new(params[:entry]) + # if @entry.save + # # The entry was saved correctly, redirect to show + # redirect_to action: 'show', id: @entry.id + # else + # # things didn't go so well, do something else + # end # end - # end # - # In this case, after saving our new entry to the database, the user is redirected to the show method, which is then executed. - # Note that this is an external HTTP-level redirection which will cause the browser to make a second request (a GET to the show action), - # and not some internal re-routing which calls both "create" and then "show" within one request. + # In this case, after saving our new entry to the database, the user is + # redirected to the `show` method, which is then executed. Note that this is an + # external HTTP-level redirection which will cause the browser to make a second + # request (a GET to the show action), and not some internal re-routing which + # calls both "create" and then "show" within one request. # - # Learn more about redirect_to and what options you have in ActionController::Redirecting. + # Learn more about `redirect_to` and what options you have in + # ActionController::Redirecting. # - # == Calling multiple redirects or renders + # ## Calling multiple redirects or renders # - # An action may contain only a single render or a single redirect. Attempting to try to do either again will result in a DoubleRenderError: + # An action may perform only a single render or a single redirect. Attempting to + # do either again will result in a DoubleRenderError: # - # def do_something - # redirect_to action: "elsewhere" - # render action: "overthere" # raises DoubleRenderError - # end + # def do_something + # redirect_to action: "elsewhere" + # render action: "overthere" # raises DoubleRenderError + # end # - # If you need to redirect on the condition of something, then be sure to add "and return" to halt execution. + # If you need to redirect on the condition of something, then be sure to add + # "return" to halt execution. # - # def do_something - # redirect_to(action: "elsewhere") and return if monkeys.nil? - # render action: "overthere" # won't be called if monkeys is nil - # end + # def do_something + # if monkeys.nil? + # redirect_to(action: "elsewhere") + # return + # end + # render action: "overthere" # won't be called if monkeys is nil + # end # class Base < Metal abstract! - # We document the request and response methods here because albeit they are - # implemented in ActionController::Metal, the type of the returned objects - # is unknown at that level. - - ## - # :method: request - # - # Returns an ActionDispatch::Request instance that represents the - # current request. - - ## - # :method: response - # - # Returns an ActionDispatch::Response that represents the current - # response. - # Shortcut helper that returns all the modules included in # ActionController::Base except the ones passed as arguments: # - # class MyBaseController < ActionController::Metal - # ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left| - # include left + # class MyBaseController < ActionController::Metal + # ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left| + # include left + # end # end - # end # - # This gives better control over what you want to exclude and makes it - # easier to create a bare controller class, instead of listing the modules - # required manually. + # This gives better control over what you want to exclude and makes it easier to + # create a bare controller class, instead of listing the modules required + # manually. def self.without_modules(*modules) modules = modules.map do |m| m.is_a?(Symbol) ? ActionController.const_get(m) : m @@ -206,7 +231,6 @@ def self.without_modules(*modules) AbstractController::Rendering, AbstractController::Translation, AbstractController::AssetPaths, - Helpers, UrlFor, Redirecting, @@ -226,8 +250,9 @@ def self.without_modules(*modules) FormBuilder, RequestForgeryProtection, ContentSecurityPolicy, - FeaturePolicy, - ForceSSL, + PermissionsPolicy, + RateLimiting, + AllowBrowser, Streaming, DataStreaming, HttpAuthentication::Basic::ControllerMethods, @@ -235,32 +260,65 @@ def self.without_modules(*modules) HttpAuthentication::Token::ControllerMethods, DefaultHeaders, Logging, - - # Before callbacks should also be executed as early as possible, so - # also include them at the bottom. AbstractController::Callbacks, - - # Append rescue at the bottom to wrap as much as possible. Rescue, - - # Add instrumentations hooks at the bottom, to ensure they instrument - # all the methods properly. Instrumentation, - - # Params wrapper should come before instrumentation so they are - # properly showed in logs ParamsWrapper ] - MODULES.each do |mod| - include mod - end + # Note: Documenting these severely degrades the performance of rdoc + # :stopdoc: + include AbstractController::Rendering + include AbstractController::Translation + include AbstractController::AssetPaths + include Helpers + include UrlFor + include Redirecting + include ActionView::Layouts + include Rendering + include Renderers::All + include ConditionalGet + include EtagWithTemplateDigest + include EtagWithFlash + include Caching + include MimeResponds + include ImplicitRender + include StrongParameters + include ParameterEncoding + include Cookies + include Flash + include FormBuilder + include RequestForgeryProtection + include ContentSecurityPolicy + include PermissionsPolicy + include RateLimiting + include AllowBrowser + include Streaming + include DataStreaming + include HttpAuthentication::Basic::ControllerMethods + include HttpAuthentication::Digest::ControllerMethods + include HttpAuthentication::Token::ControllerMethods + include DefaultHeaders + include Logging + # Before callbacks should also be executed as early as possible, so also include + # them at the bottom. + include AbstractController::Callbacks + # Append rescue at the bottom to wrap as much as possible. + include Rescue + # Add instrumentations hooks at the bottom, to ensure they instrument all the + # methods properly. + include Instrumentation + # Params wrapper should come before instrumentation so they are properly showed + # in logs + include ParamsWrapper + # :startdoc: setup_renderer! # Define some internal variables that should not be propagated to the view. PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + %i( @_params @_response @_request @_config @_url_options @_action_has_layout @_view_context_class @_view_renderer @_lookup_context @_routes @_view_runtime @_db_runtime @_helper_proxy + @_marked_for_same_origin_verification @_rendered_format ) def _protected_ivars diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 83e3e0c37c857..4a700952df12b 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -1,28 +1,31 @@ # frozen_string_literal: true +# :markup: markdown + module ActionController - # \Caching is a cheap way of speeding up slow applications by keeping the result of - # calculations, renderings, and database calls around for subsequent requests. + # # Action Controller Caching # - # You can read more about each approach by clicking the modules below. + # Caching is a cheap way of speeding up slow applications by keeping the result + # of calculations, renderings, and database calls around for subsequent + # requests. # # Note: To turn off all caching provided by Action Controller, set - # config.action_controller.perform_caching = false # - # == \Caching stores + # config.action_controller.perform_caching = false + # + # ## Caching stores # - # All the caching stores from ActiveSupport::Cache are available to be used as backends - # for Action Controller caching. + # All the caching stores from ActiveSupport::Cache are available to be used as + # backends for Action Controller caching. # # Configuration examples (FileStore is the default): # - # config.action_controller.cache_store = :memory_store - # config.action_controller.cache_store = :file_store, '/path/to/cache/directory' - # config.action_controller.cache_store = :mem_cache_store, 'localhost' - # config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new('localhost:11211') - # config.action_controller.cache_store = MyOwnStore.new('parameter') + # config.action_controller.cache_store = :memory_store + # config.action_controller.cache_store = :file_store, '/path/to/cache/directory' + # config.action_controller.cache_store = :mem_cache_store, 'localhost' + # config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new('localhost:11211') + # config.action_controller.cache_store = MyOwnStore.new('parameter') module Caching - extend ActiveSupport::Autoload extend ActiveSupport::Concern included do diff --git a/actionpack/lib/action_controller/deprecator.rb b/actionpack/lib/action_controller/deprecator.rb new file mode 100644 index 0000000000000..3c0ea2a2fb512 --- /dev/null +++ b/actionpack/lib/action_controller/deprecator.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# :markup: markdown + +module ActionController + def self.deprecator # :nodoc: + AbstractController.deprecator + end +end diff --git a/actionpack/lib/action_controller/form_builder.rb b/actionpack/lib/action_controller/form_builder.rb index 09d2ac1837cb2..a14a6e9e19234 100644 --- a/actionpack/lib/action_controller/form_builder.rb +++ b/actionpack/lib/action_controller/form_builder.rb @@ -1,29 +1,33 @@ # frozen_string_literal: true +# :markup: markdown + module ActionController - # Override the default form builder for all views rendered by this - # controller and any of its descendants. Accepts a subclass of - # +ActionView::Helpers::FormBuilder+. + # # Action Controller Form Builder + # + # Override the default form builder for all views rendered by this controller + # and any of its descendants. Accepts a subclass of + # ActionView::Helpers::FormBuilder. # # For example, given a form builder: # - # class AdminFormBuilder < ActionView::Helpers::FormBuilder - # def special_field(name) + # class AdminFormBuilder < ActionView::Helpers::FormBuilder + # def special_field(name) + # end # end - # end # # The controller specifies a form builder as its default: # - # class AdminAreaController < ApplicationController - # default_form_builder AdminFormBuilder - # end + # class AdminAreaController < ApplicationController + # default_form_builder AdminFormBuilder + # end # - # Then in the view any form using +form_for+ will be an instance of the - # specified form builder: + # Then in the view any form using `form_with` or `form_for` will be an + # instance of the specified form builder: # - # <%= form_for(@instance) do |builder| %> - # <%= builder.special_field(:name) %> - # <% end %> + # <%= form_with(model: @instance) do |builder| %> + # <%= builder.special_field(:name) %> + # <% end %> module FormBuilder extend ActiveSupport::Concern @@ -32,11 +36,12 @@ module FormBuilder end module ClassMethods - # Set the form builder to be used as the default for all forms - # in the views rendered by this controller and its subclasses. + # Set the form builder to be used as the default for all forms in the views + # rendered by this controller and its subclasses. # - # ==== Parameters - # * builder - Default form builder, an instance of +ActionView::Helpers::FormBuilder+ + # #### Parameters + # * `builder` - Default form builder. Accepts a subclass of + # ActionView::Helpers::FormBuilder def default_form_builder(builder) self._default_form_builder = builder end diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index be74163230bf1..02f8493cb68d6 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionController class LogSubscriber < ActiveSupport::LogSubscriber INTERNAL_PARAMS = %w(controller action format _method only_path) @@ -8,7 +10,10 @@ def start_processing(event) return unless logger.info? payload = event.payload - params = payload[:params].except(*INTERNAL_PARAMS) + params = {} + payload[:params].each_pair do |k, v| + params[k] = v unless INTERNAL_PARAMS.include?(k) + end format = payload[:format] format = format.to_s.upcase if format.is_a?(Symbol) format = "*/*" if format.nil? @@ -16,6 +21,7 @@ def start_processing(event) info "Processing by #{payload[:controller]}##{payload[:action]} as #{format}" info " Parameters: #{params.inspect}" unless params.empty? end + subscribe_log_level :start_processing, :info def process_action(event) info do @@ -23,52 +29,61 @@ def process_action(event) additions = ActionController::Base.log_process_action(payload) status = payload[:status] - if status.nil? && (exception_class_name = payload[:exception].first) + if status.nil? && (exception_class_name = payload[:exception]&.first) status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) end - additions << "Allocations: #{event.allocations}" + additions << "GC: #{event.gc_time.round(1)}ms" - message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" - message << " (#{additions.join(" | ")})" + message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" \ + " (#{additions.join(" | ")})" message << "\n\n" if defined?(Rails.env) && Rails.env.development? message end end + subscribe_log_level :process_action, :info def halted_callback(event) info { "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected" } end + subscribe_log_level :halted_callback, :info def send_file(event) info { "Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)" } end + subscribe_log_level :send_file, :info def redirect_to(event) info { "Redirected to #{event.payload[:location]}" } end + subscribe_log_level :redirect_to, :info def send_data(event) info { "Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)" } end + subscribe_log_level :send_data, :info def unpermitted_parameters(event) debug do unpermitted_keys = event.payload[:keys] - color("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.map { |e| ":#{e}" }.join(", ")}", RED) + display_unpermitted_keys = unpermitted_keys.map { |e| ":#{e}" }.join(", ") + context = event.payload[:context].map { |k, v| "#{k}: #{v}" }.join(", ") + color("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{display_unpermitted_keys}. Context: { #{context} }", RED) end end + subscribe_log_level :unpermitted_parameters, :debug - %w(write_fragment read_fragment exist_fragment? - expire_fragment expire_page write_page).each do |method| + %w(write_fragment read_fragment exist_fragment? expire_fragment).each do |method| class_eval <<-METHOD, __FILE__, __LINE__ + 1 + # frozen_string_literal: true def #{method}(event) - return unless logger.info? && ActionController::Base.enable_fragment_cache_logging + return unless ActionController::Base.enable_fragment_cache_logging key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path]) human_name = #{method.to_s.humanize.inspect} info("\#{human_name} \#{key} (\#{event.duration.round(1)}ms)") end + subscribe_log_level :#{method}, :info METHOD end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index f0821313f9d0f..4c58735fa2911 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -1,20 +1,22 @@ # frozen_string_literal: true +# :markup: markdown + require "active_support/core_ext/array/extract_options" require "action_dispatch/middleware/stack" -require "action_dispatch/http/request" -require "action_dispatch/http/response" module ActionController - # Extend ActionDispatch middleware stack to make it aware of options - # allowing the following syntax in controllers: + # # Action Controller MiddlewareStack + # + # Extend ActionDispatch middleware stack to make it aware of options allowing + # the following syntax in controllers: # - # class PostsController < ApplicationController - # use AuthenticationMiddleware, except: [:index, :show] - # end + # class PostsController < ApplicationController + # use AuthenticationMiddleware, except: [:index, :show] + # end # - class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc: - class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc: + class MiddlewareStack < ActionDispatch::MiddlewareStack # :nodoc: + class Middleware < ActionDispatch::MiddlewareStack::Middleware # :nodoc: def initialize(klass, args, actions, strategy, block) @actions = actions @strategy = strategy @@ -60,73 +62,73 @@ def build_middleware(klass, args, block) end end - # ActionController::Metal is the simplest possible controller, providing a + # # Action Controller Metal + # + # `ActionController::Metal` is the simplest possible controller, providing a # valid Rack interface without the additional niceties provided by - # ActionController::Base. + # ActionController::Base. # # A sample metal controller might look like this: # - # class HelloController < ActionController::Metal - # def index - # self.response_body = "Hello World!" + # class HelloController < ActionController::Metal + # def index + # self.response_body = "Hello World!" + # end # end - # end # - # And then to route requests to your metal controller, you would add - # something like this to config/routes.rb: + # And then to route requests to your metal controller, you would add something + # like this to `config/routes.rb`: # - # get 'hello', to: HelloController.action(:index) + # get 'hello', to: HelloController.action(:index) # - # The +action+ method returns a valid Rack application for the \Rails - # router to dispatch to. + # The ::action method returns a valid Rack application for the Rails router to + # dispatch to. # - # == Rendering Helpers + # ## Rendering Helpers # - # ActionController::Metal by default provides no utilities for rendering - # views, partials, or other responses aside from explicitly calling of - # response_body=, content_type=, and status=. To - # add the render helpers you're used to having in a normal controller, you - # can do the following: + # By default, `ActionController::Metal` provides no utilities for rendering + # views, partials, or other responses aside from some low-level setters such + # as #response_body=, #content_type=, and #status=. To add the render helpers + # you're used to having in a normal controller, you can do the following: # - # class HelloController < ActionController::Metal - # include AbstractController::Rendering - # include ActionView::Layouts - # append_view_path "#{Rails.root}/app/views" + # class HelloController < ActionController::Metal + # include AbstractController::Rendering + # include ActionView::Layouts + # append_view_path "#{Rails.root}/app/views" # - # def index - # render "hello/index" + # def index + # render "hello/index" + # end # end - # end # - # == Redirection Helpers + # ## Redirection Helpers # # To add redirection helpers to your metal controller, do the following: # - # class HelloController < ActionController::Metal - # include ActionController::Redirecting - # include Rails.application.routes.url_helpers + # class HelloController < ActionController::Metal + # include ActionController::Redirecting + # include Rails.application.routes.url_helpers # - # def index - # redirect_to root_url + # def index + # redirect_to root_url + # end # end - # end - # - # == Other Helpers # - # You can refer to the modules included in ActionController::Base to see - # other features you can bring into your metal controller. + # ## Other Helpers # + # You can refer to the modules included in ActionController::Base to see other + # features you can bring into your metal controller. class Metal < AbstractController::Base abstract! - # Returns the last part of the controller's name, underscored, without the ending - # Controller. For instance, PostsController returns posts. - # Namespaces are left out, so Admin::PostsController returns posts as well. + # Returns the last part of the controller's name, underscored, without the + # ending `Controller`. For instance, `PostsController` returns `posts`. + # Namespaces are left out, so `Admin::PostsController` returns `posts` as well. # - # ==== Returns - # * string + # #### Returns + # * `string` def self.controller_name - @controller_name ||= name.demodulize.sub(/Controller$/, "").underscore + @controller_name ||= (name.demodulize.delete_suffix("Controller").underscore unless anonymous?) end def self.make_response!(request) @@ -135,24 +137,82 @@ def self.make_response!(request) end end - def self.binary_params_for?(action) # :nodoc: + def self.action_encoding_template(action) # :nodoc: false end - # Delegates to the class' controller_name. + class << self + private + def inherited(subclass) + super + subclass.middleware_stack = middleware_stack.dup + subclass.class_eval do + @controller_name = nil + end + end + end + + # Delegates to the class's ::controller_name. def controller_name self.class.controller_name end - attr_internal :response, :request + ## + # :attr_reader: request + # + # The ActionDispatch::Request instance for the current request. + attr_internal :request + + ## + # :attr_reader: response + # + # The ActionDispatch::Response instance for the current response. + attr_internal_reader :response + + ## + # The ActionDispatch::Request::Session instance for the current request. + # See further details in the + # [Active Controller Session guide](https://guides.rubyonrails.org/action_controller_overview.html#session). delegate :session, to: "@_request" - delegate :headers, :status=, :location=, :content_type=, - :status, :location, :content_type, :media_type, to: "@_response" + + ## + # Delegates to ActionDispatch::Response#headers. + delegate :headers, to: "@_response" + + ## + # Delegates to ActionDispatch::Response#status= + delegate :status=, to: "@_response" + + ## + # Delegates to ActionDispatch::Response#location= + delegate :location=, to: "@_response" + + ## + # Delegates to ActionDispatch::Response#content_type= + delegate :content_type=, to: "@_response" + + ## + # Delegates to ActionDispatch::Response#status + delegate :status, to: "@_response" + + ## + # Delegates to ActionDispatch::Response#location + delegate :location, to: "@_response" + + ## + # Delegates to ActionDispatch::Response#content_type + delegate :content_type, to: "@_response" + + ## + # Delegates to ActionDispatch::Response#media_type + delegate :media_type, to: "@_response" def initialize @_request = nil @_response = nil + @_response_body = nil @_routes = nil + @_params = nil super end @@ -166,17 +226,19 @@ def params=(val) alias :response_code :status # :nodoc: - # Basic url_for that can be overridden for more robust functionality. + # Basic `url_for` that can be overridden for more robust functionality. def url_for(string) string end def response_body=(body) - body = [body] unless body.nil? || body.respond_to?(:each) - response.reset_body! - return unless body - response.body = body - super + if body + body = [body] if body.is_a?(String) + response.body = body + super + else + response.reset_body! + end end # Tests if render or redirect has already happened. @@ -184,7 +246,7 @@ def performed? response_body || response.committed? end - def dispatch(name, request, response) #:nodoc: + def dispatch(name, request, response) # :nodoc: set_request!(request) set_response!(response) process(name) @@ -193,15 +255,29 @@ def dispatch(name, request, response) #:nodoc: end def set_response!(response) # :nodoc: + if @_response + _, _, body = @_response + body.close if body.respond_to?(:close) + end + @_response = response end - def set_request!(request) #:nodoc: + # Assign the response and mark it as committed. No further processing will + # occur. + def response=(response) + set_response!(response) + + # Force `performed?` to return true: + @_response_body = true + end + + def set_request!(request) # :nodoc: @_request = request @_request.controller_instance = self end - def to_a #:nodoc: + def to_a # :nodoc: response.to_a end @@ -211,21 +287,26 @@ def reset_session class_attribute :middleware_stack, default: ActionController::MiddlewareStack.new - def self.inherited(base) # :nodoc: - base.middleware_stack = middleware_stack.dup - super - end - class << self # Pushes the given Rack middleware and its arguments to the bottom of the # middleware stack. - def use(*args, &block) - middleware_stack.use(*args, &block) + def use(...) + middleware_stack.use(...) end - ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true) end - # Alias for +middleware_stack+. + # The middleware stack used by this controller. + # + # By default uses a variation of ActionDispatch::MiddlewareStack which allows + # for the following syntax: + # + # class PostsController < ApplicationController + # use AuthenticationMiddleware, except: [:index, :show] + # end + # + # Read more about [Rails middleware stack] + # (https://guides.rubyonrails.org/rails_on_rack.html#action-dispatcher-middleware-stack) + # in the guides. def self.middleware middleware_stack end @@ -245,8 +326,8 @@ def self.action(name) end end - # Direct dispatch to the controller. Instantiates the controller, then - # executes the action named +name+. + # Direct dispatch to the controller. Instantiates the controller, then executes + # the action named `name`. def self.dispatch(name, req, res) if middleware_stack.any? middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env diff --git a/actionpack/lib/action_controller/metal/allow_browser.rb b/actionpack/lib/action_controller/metal/allow_browser.rb new file mode 100644 index 0000000000000..f32a7cb91b08b --- /dev/null +++ b/actionpack/lib/action_controller/metal/allow_browser.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +# :markup: markdown + +module ActionController # :nodoc: + module AllowBrowser + extend ActiveSupport::Concern + + module ClassMethods + # Specify the browser versions that will be allowed to access all actions (or + # some, as limited by `only:` or `except:`). Only browsers matched in the hash + # or named set passed to `versions:` will be blocked if they're below the + # versions specified. This means that all other browsers, as well as agents that + # aren't reporting a user-agent header, will be allowed access. + # + # A browser that's blocked will by default be served the file in + # public/406-unsupported-browser.html with an HTTP status code of "406 Not + # Acceptable". + # + # In addition to specifically named browser versions, you can also pass + # `:modern` as the set to restrict support to browsers natively supporting webp + # images, web push, badges, import maps, CSS nesting, and CSS :has. This + # includes Safari 17.2+, Chrome 120+, Firefox 121+, Opera 106+. + # + # You can use https://caniuse.com to check for browser versions supporting the + # features you use. + # + # You can use `ActiveSupport::Notifications` to subscribe to events of browsers + # being blocked using the `browser_block.action_controller` event name. + # + # Examples: + # + # class ApplicationController < ActionController::Base + # # Allow only browsers natively supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has + # allow_browser versions: :modern + # end + # + # class ApplicationController < ActionController::Base + # # Allow only browsers natively supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has + # allow_browser versions: :modern, block: :handle_outdated_browser + # + # private + # def handle_outdated_browser + # render file: Rails.root.join("public/custom-error.html"), status: :not_acceptable + # end + # end + # + # class ApplicationController < ActionController::Base + # # All versions of Chrome and Opera will be allowed, but no versions of "internet explorer" (ie). Safari needs to be 16.4+ and Firefox 121+. + # allow_browser versions: { safari: 16.4, firefox: 121, ie: false } + # end + # + # class MessagesController < ApplicationController + # # In addition to the browsers blocked by ApplicationController, also block Opera below 104 and Chrome below 119 for the show action. + # allow_browser versions: { opera: 104, chrome: 119 }, only: :show + # end + def allow_browser(versions:, block: -> { render file: Rails.root.join("public/406-unsupported-browser.html"), layout: false, status: :not_acceptable }, **options) + before_action -> { allow_browser(versions: versions, block: block) }, **options + end + end + + private + def allow_browser(versions:, block:) + require "useragent" + + if BrowserBlocker.new(request, versions: versions).blocked? + ActiveSupport::Notifications.instrument("browser_block.action_controller", request: request, versions: versions) do + block.is_a?(Symbol) ? send(block) : instance_exec(&block) + end + end + end + + class BrowserBlocker # :nodoc: + SETS = { + modern: { safari: 17.2, chrome: 120, firefox: 121, opera: 106, ie: false } + } + + attr_reader :request, :versions + + def initialize(request, versions:) + @request, @versions = request, versions + end + + def blocked? + user_agent_version_reported? && unsupported_browser? + end + + private + def parsed_user_agent + @parsed_user_agent ||= UserAgent.parse(request.user_agent) + end + + def user_agent_version_reported? + request.user_agent.present? && parsed_user_agent.version.to_s.present? + end + + def unsupported_browser? + version_guarded_browser? && version_below_minimum_required? && !bot? + end + + def version_guarded_browser? + minimum_browser_version_for_browser != nil + end + + def bot? + parsed_user_agent.bot? + end + + def version_below_minimum_required? + if minimum_browser_version_for_browser + parsed_user_agent.version < UserAgent::Version.new(minimum_browser_version_for_browser.to_s) + else + true + end + end + + def minimum_browser_version_for_browser + expanded_versions[normalized_browser_name] + end + + def expanded_versions + @expanded_versions ||= (SETS[versions] || versions).with_indifferent_access + end + + def normalized_browser_name + case name = parsed_user_agent.browser.downcase + when "internet explorer" then "ie" + else name + end + end + end + end +end diff --git a/actionpack/lib/action_controller/metal/basic_implicit_render.rb b/actionpack/lib/action_controller/metal/basic_implicit_render.rb index f9a758ff0e287..08230e93f940b 100644 --- a/actionpack/lib/action_controller/metal/basic_implicit_render.rb +++ b/actionpack/lib/action_controller/metal/basic_implicit_render.rb @@ -1,9 +1,13 @@ # frozen_string_literal: true +# :markup: markdown + module ActionController module BasicImplicitRender # :nodoc: def send_action(method, *args) - super.tap { default_render unless performed? } + ret = super + default_render unless performed? + ret end def default_render diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 6371abea58bf7..2f172279a312c 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + require "active_support/core_ext/object/try" require "active_support/core_ext/integer/time" @@ -14,96 +16,126 @@ module ConditionalGet end module ClassMethods - # Allows you to consider additional controller-wide information when generating an ETag. - # For example, if you serve pages tailored depending on who's logged in at the moment, you - # may want to add the current user id to be part of the ETag to prevent unauthorized displaying - # of cached pages. + # Allows you to consider additional controller-wide information when generating + # an ETag. For example, if you serve pages tailored depending on who's logged in + # at the moment, you may want to add the current user id to be part of the ETag + # to prevent unauthorized displaying of cached pages. # - # class InvoicesController < ApplicationController - # etag { current_user&.id } + # class InvoicesController < ApplicationController + # etag { current_user&.id } # - # def show - # # Etag will differ even for the same invoice when it's viewed by a different current_user - # @invoice = Invoice.find(params[:id]) - # fresh_when etag: @invoice + # def show + # # Etag will differ even for the same invoice when it's viewed by a different current_user + # @invoice = Invoice.find(params[:id]) + # fresh_when etag: @invoice + # end # end - # end def etag(&etagger) self.etaggers += [etagger] end end - # Sets the +etag+, +last_modified+, or both on the response and renders a - # 304 Not Modified response if the request is already fresh. - # - # === Parameters: - # - # * :etag Sets a "weak" ETag validator on the response. See the - # +:weak_etag+ option. - # * :weak_etag Sets a "weak" ETag validator on the response. - # Requests that set If-None-Match header may return a 304 Not Modified - # response if it matches the ETag exactly. A weak ETag indicates semantic - # equivalence, not byte-for-byte equality, so they're good for caching - # HTML pages in browser caches. They can't be used for responses that - # must be byte-identical, like serving Range requests within a PDF file. - # * :strong_etag Sets a "strong" ETag validator on the response. - # Requests that set If-None-Match header may return a 304 Not Modified - # response if it matches the ETag exactly. A strong ETag implies exact - # equality: the response must match byte for byte. This is necessary for - # doing Range requests within a large video or PDF file, for example, or - # for compatibility with some CDNs that don't support weak ETags. - # * :last_modified Sets a "weak" last-update validator on the - # response. Subsequent requests that set If-Modified-Since may return a - # 304 Not Modified response if last_modified <= If-Modified-Since. - # * :public By default the Cache-Control header is private, set this to - # +true+ if you want your application to be cacheable by other devices (proxy caches). - # * :template By default, the template digest for the current - # controller/action is included in ETags. If the action renders a - # different template, you can include its digest instead. If the action - # doesn't render a template at all, you can pass template: false - # to skip any attempt to check for a template digest. - # - # === Example: - # - # def show - # @article = Article.find(params[:id]) - # fresh_when(etag: @article, last_modified: @article.updated_at, public: true) - # end - # - # This will render the show template if the request isn't sending a matching ETag or - # If-Modified-Since header and just a 304 Not Modified response if there's a match. - # - # You can also just pass a record. In this case +last_modified+ will be set - # by calling +updated_at+ and +etag+ by passing the object itself. - # - # def show - # @article = Article.find(params[:id]) - # fresh_when(@article) - # end - # - # You can also pass an object that responds to +maximum+, such as a - # collection of active records. In this case +last_modified+ will be set by - # calling maximum(:updated_at) on the collection (the timestamp of the - # most recently updated record) and the +etag+ by passing the object itself. - # - # def index - # @articles = Article.all - # fresh_when(@articles) - # end - # - # When passing a record or a collection, you can still set the public header: - # - # def show - # @article = Article.find(params[:id]) - # fresh_when(@article, public: true) - # end - # - # When rendering a different template than the default controller/action - # style, you can indicate which digest to include in the ETag: - # - # before_action { fresh_when @article, template: 'widgets/show' } - # - def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, template: nil) + # Sets the `etag`, `last_modified`, or both on the response, and renders a `304 + # Not Modified` response if the request is already fresh. + # + # #### Options + # + # `:etag` + # : Sets a "weak" ETag validator on the response. See the `:weak_etag` option. + # + # `:weak_etag` + # : Sets a "weak" ETag validator on the response. Requests that specify an + # `If-None-Match` header may receive a `304 Not Modified` response if the + # ETag matches exactly. + # + # : A weak ETag indicates semantic equivalence, not byte-for-byte equality, so + # they're good for caching HTML pages in browser caches. They can't be used + # for responses that must be byte-identical, like serving `Range` requests + # within a PDF file. + # + # `:strong_etag` + # : Sets a "strong" ETag validator on the response. Requests that specify an + # `If-None-Match` header may receive a `304 Not Modified` response if the + # ETag matches exactly. + # + # : A strong ETag implies exact equality -- the response must match byte for + # byte. This is necessary for serving `Range` requests within a large video + # or PDF file, for example, or for compatibility with some CDNs that don't + # support weak ETags. + # + # `:last_modified` + # : Sets a "weak" last-update validator on the response. Subsequent requests + # that specify an `If-Modified-Since` header may receive a `304 Not + # Modified` response if `last_modified` <= `If-Modified-Since`. + # + # `:public` + # : By default the `Cache-Control` header is private. Set this option to + # `true` if you want your application to be cacheable by other devices, such + # as proxy caches. + # + # `:cache_control` + # : When given, will overwrite an existing `Cache-Control` header. For a list + # of `Cache-Control` directives, see the [article on + # MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control). + # + # `:template` + # : By default, the template digest for the current controller/action is + # included in ETags. If the action renders a different template, you can + # include its digest instead. If the action doesn't render a template at + # all, you can pass `template: false` to skip any attempt to check for a + # template digest. + # + # + # #### Examples + # + # def show + # @article = Article.find(params[:id]) + # fresh_when(etag: @article, last_modified: @article.updated_at, public: true) + # end + # + # This will send a `304 Not Modified` response if the request specifies a + # matching ETag and `If-Modified-Since` header. Otherwise, it will render the + # `show` template. + # + # You can also just pass a record: + # + # def show + # @article = Article.find(params[:id]) + # fresh_when(@article) + # end + # + # `etag` will be set to the record, and `last_modified` will be set to the + # record's `updated_at`. + # + # You can also pass an object that responds to `maximum`, such as a collection + # of records: + # + # def index + # @articles = Article.all + # fresh_when(@articles) + # end + # + # In this case, `etag` will be set to the collection, and `last_modified` will + # be set to `maximum(:updated_at)` (the timestamp of the most recently updated + # record). + # + # When passing a record or a collection, you can still specify other options, + # such as `:public` and `:cache_control`: + # + # def show + # @article = Article.find(params[:id]) + # fresh_when(@article, public: true, cache_control: { no_cache: true }) + # end + # + # The above will set `Cache-Control: public, no-cache` in the response. + # + # When rendering a different template than the controller/action's default + # template, you can indicate which digest to include in the ETag: + # + # before_action { fresh_when @article, template: "widgets/show" } + # + def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, cache_control: {}, template: nil) + response.cache_control.delete(:no_store) weak_etag ||= etag || object unless strong_etag last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at) @@ -117,139 +149,153 @@ def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_m response.last_modified = last_modified if last_modified response.cache_control[:public] = true if public + response.cache_control.merge!(cache_control) head :not_modified if request.fresh?(response) end - # Sets the +etag+ and/or +last_modified+ on the response and checks it against - # the client request. If the request doesn't match the options provided, the - # request is considered stale and should be generated from scratch. Otherwise, - # it's fresh and we don't need to generate anything and a reply of 304 Not Modified is sent. - # - # === Parameters: - # - # * :etag Sets a "weak" ETag validator on the response. See the - # +:weak_etag+ option. - # * :weak_etag Sets a "weak" ETag validator on the response. - # Requests that set If-None-Match header may return a 304 Not Modified - # response if it matches the ETag exactly. A weak ETag indicates semantic - # equivalence, not byte-for-byte equality, so they're good for caching - # HTML pages in browser caches. They can't be used for responses that - # must be byte-identical, like serving Range requests within a PDF file. - # * :strong_etag Sets a "strong" ETag validator on the response. - # Requests that set If-None-Match header may return a 304 Not Modified - # response if it matches the ETag exactly. A strong ETag implies exact - # equality: the response must match byte for byte. This is necessary for - # doing Range requests within a large video or PDF file, for example, or - # for compatibility with some CDNs that don't support weak ETags. - # * :last_modified Sets a "weak" last-update validator on the - # response. Subsequent requests that set If-Modified-Since may return a - # 304 Not Modified response if last_modified <= If-Modified-Since. - # * :public By default the Cache-Control header is private, set this to - # +true+ if you want your application to be cacheable by other devices (proxy caches). - # * :template By default, the template digest for the current - # controller/action is included in ETags. If the action renders a - # different template, you can include its digest instead. If the action - # doesn't render a template at all, you can pass template: false - # to skip any attempt to check for a template digest. - # - # === Example: - # - # def show - # @article = Article.find(params[:id]) - # - # if stale?(etag: @article, last_modified: @article.updated_at) - # @statistics = @article.really_expensive_call - # respond_to do |format| - # # all the supported formats + # Sets the `etag` and/or `last_modified` on the response and checks them against + # the request. If the request doesn't match the provided options, it is + # considered stale, and the response should be rendered from scratch. Otherwise, + # it is fresh, and a `304 Not Modified` is sent. + # + # #### Options + # + # See #fresh_when for supported options. + # + # #### Examples + # + # def show + # @article = Article.find(params[:id]) + # + # if stale?(etag: @article, last_modified: @article.updated_at) + # @statistics = @article.really_expensive_call + # respond_to do |format| + # # all the supported formats + # end # end # end - # end # - # You can also just pass a record. In this case +last_modified+ will be set - # by calling +updated_at+ and +etag+ by passing the object itself. + # You can also just pass a record: # - # def show - # @article = Article.find(params[:id]) + # def show + # @article = Article.find(params[:id]) # - # if stale?(@article) - # @statistics = @article.really_expensive_call - # respond_to do |format| - # # all the supported formats + # if stale?(@article) + # @statistics = @article.really_expensive_call + # respond_to do |format| + # # all the supported formats + # end # end # end - # end # - # You can also pass an object that responds to +maximum+, such as a - # collection of active records. In this case +last_modified+ will be set by - # calling +maximum(:updated_at)+ on the collection (the timestamp of the - # most recently updated record) and the +etag+ by passing the object itself. + # `etag` will be set to the record, and `last_modified` will be set to the + # record's `updated_at`. + # + # You can also pass an object that responds to `maximum`, such as a collection + # of records: # - # def index - # @articles = Article.all + # def index + # @articles = Article.all # - # if stale?(@articles) - # @statistics = @articles.really_expensive_call - # respond_to do |format| - # # all the supported formats + # if stale?(@articles) + # @statistics = @articles.really_expensive_call + # respond_to do |format| + # # all the supported formats + # end # end # end - # end # - # When passing a record or a collection, you can still set the public header: + # In this case, `etag` will be set to the collection, and `last_modified` will + # be set to `maximum(:updated_at)` (the timestamp of the most recently updated + # record). # - # def show - # @article = Article.find(params[:id]) + # When passing a record or a collection, you can still specify other options, + # such as `:public` and `:cache_control`: # - # if stale?(@article, public: true) - # @statistics = @article.really_expensive_call - # respond_to do |format| - # # all the supported formats + # def show + # @article = Article.find(params[:id]) + # + # if stale?(@article, public: true, cache_control: { no_cache: true }) + # @statistics = @articles.really_expensive_call + # respond_to do |format| + # # all the supported formats + # end # end # end - # end # - # When rendering a different template than the default controller/action - # style, you can indicate which digest to include in the ETag: + # The above will set `Cache-Control: public, no-cache` in the response. + # + # When rendering a different template than the controller/action's default + # template, you can indicate which digest to include in the ETag: # - # def show - # super if stale? @article, template: 'widgets/show' - # end + # def show + # super if stale?(@article, template: "widgets/show") + # end # def stale?(object = nil, **freshness_kwargs) fresh_when(object, **freshness_kwargs) !request.fresh?(response) end - # Sets an HTTP 1.1 Cache-Control header. Defaults to issuing a +private+ - # instruction, so that intermediate caches must not cache the response. + # Sets the `Cache-Control` header, overwriting existing directives. This method + # will also ensure an HTTP `Date` header for client compatibility. + # + # Defaults to issuing the `private` directive, so that intermediate caches must + # not cache the response. + # + # #### Options + # + # `:public` + # : If true, replaces the default `private` directive with the `public` + # directive. + # + # `:must_revalidate` + # : If true, adds the `must-revalidate` directive. + # + # `:stale_while_revalidate` + # : Sets the value of the `stale-while-revalidate` directive. + # + # `:stale_if_error` + # : Sets the value of the `stale-if-error` directive. + # + # `:immutable` + # : If true, adds the `immutable` directive. + # + # + # Any additional key-value pairs are concatenated as directives. For a list of + # supported `Cache-Control` directives, see the [article on + # MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control). # - # expires_in 20.minutes - # expires_in 3.hours, public: true - # expires_in 3.hours, public: true, must_revalidate: true + # #### Examples # - # This method will overwrite an existing Cache-Control header. - # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities. + # expires_in 10.minutes + # # => Cache-Control: max-age=600, private # - # HTTP Cache-Control Extensions for Stale Content. See https://tools.ietf.org/html/rfc5861 - # It helps to cache an asset and serve it while is being revalidated and/or returning with an error. + # expires_in 10.minutes, public: true + # # => Cache-Control: max-age=600, public # - # expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds - # expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds, stale_if_error: 5.minutes + # expires_in 10.minutes, public: true, must_revalidate: true + # # => Cache-Control: max-age=600, public, must-revalidate # - # HTTP Cache-Control Extensions other values: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control - # Any additional key-value pairs are concatenated onto the `Cache-Control` header in the response: + # expires_in 1.hour, stale_while_revalidate: 60.seconds + # # => Cache-Control: max-age=3600, private, stale-while-revalidate=60 # - # expires_in 3.hours, public: true, "s-maxage": 3.hours, "no-transform": true + # expires_in 1.hour, stale_if_error: 5.minutes + # # => Cache-Control: max-age=3600, private, stale-if-error=300 + # + # expires_in 1.hour, public: true, "s-maxage": 3.hours, "no-transform": true + # # => Cache-Control: max-age=3600, public, s-maxage=10800, no-transform=true # - # The method will also ensure an HTTP Date header for client compatibility. def expires_in(seconds, options = {}) + response.cache_control.delete(:no_store) response.cache_control.merge!( max_age: seconds, public: options.delete(:public), must_revalidate: options.delete(:must_revalidate), stale_while_revalidate: options.delete(:stale_while_revalidate), stale_if_error: options.delete(:stale_if_error), + immutable: options.delete(:immutable), ) options.delete(:private) @@ -257,8 +303,8 @@ def expires_in(seconds, options = {}) response.date = Time.now unless response.date? end - # Sets an HTTP 1.1 Cache-Control header of no-cache. This means the - # resource will be marked as stale, so clients must always revalidate. + # Sets an HTTP 1.1 `Cache-Control` header of `no-cache`. This means the resource + # will be marked as stale, so clients must always revalidate. # Intermediate/browser caches may still store the asset. def expires_now response.cache_control.replace(no_cache: true) @@ -266,20 +312,51 @@ def expires_now # Cache or yield the block. The cache is supposed to never expire. # - # You can use this method when you have an HTTP response that never changes, - # and the browser and proxies should cache it indefinitely. + # You can use this method when you have an HTTP response that never changes, and + # the browser and proxies should cache it indefinitely. # - # * +public+: By default, HTTP responses are private, cached only on the - # user's web browser. To allow proxies to cache the response, set +true+ to - # indicate that they can serve the cached response to all users. + # * `public`: By default, HTTP responses are private, cached only on the + # user's web browser. To allow proxies to cache the response, set `true` to + # indicate that they can serve the cached response to all users. def http_cache_forever(public: false) - expires_in 100.years, public: public + expires_in 100.years, public: public, immutable: true yield if stale?(etag: request.fullpath, last_modified: Time.new(2011, 1, 1).utc, public: public) end + # Sets an HTTP 1.1 `Cache-Control` header of `no-store`. This means the resource + # may not be stored in any cache. + def no_store + response.cache_control.replace(no_store: true) + end + + # Adds the `must-understand` directive to the `Cache-Control` header, which indicates + # that a cache MUST understand the semantics of the response status code that has been + # received, or discard the response. + # + # This is particularly useful when returning responses with new or uncommon + # status codes that might not be properly interpreted by older caches. + # + # #### Example + # + # def show + # @article = Article.find(params[:id]) + # + # if @article.early_access? + # must_understand + # render status: 203 # Non-Authoritative Information + # else + # fresh_when @article + # end + # end + # + def must_understand + response.cache_control[:must_understand] = true + response.cache_control[:no_store] = true + end + private def combine_etags(validator, options) [validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact diff --git a/actionpack/lib/action_controller/metal/content_security_policy.rb b/actionpack/lib/action_controller/metal/content_security_policy.rb index 25fc110bfeffa..6af6706c1cbcd 100644 --- a/actionpack/lib/action_controller/metal/content_security_policy.rb +++ b/actionpack/lib/action_controller/metal/content_security_policy.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -module ActionController #:nodoc: +# :markup: markdown + +module ActionController # :nodoc: module ContentSecurityPolicy - # TODO: Documentation extend ActiveSupport::Concern include AbstractController::Helpers @@ -14,11 +15,33 @@ module ContentSecurityPolicy end module ClassMethods + # Overrides parts of the globally configured `Content-Security-Policy` header: + # + # class PostsController < ApplicationController + # content_security_policy do |policy| + # policy.base_uri "https://www.example.com" + # end + # end + # + # Options can be passed similar to `before_action`. For example, pass `only: + # :index` to override the header on the index action only: + # + # class PostsController < ApplicationController + # content_security_policy(only: :index) do |policy| + # policy.default_src :self, :https + # end + # end + # + # Pass `false` to remove the `Content-Security-Policy` header: + # + # class PostsController < ApplicationController + # content_security_policy false, only: :index + # end def content_security_policy(enabled = true, **options, &block) before_action(options) do if block_given? policy = current_content_security_policy - yield policy + instance_exec(policy, &block) request.content_security_policy = policy end @@ -28,6 +51,18 @@ def content_security_policy(enabled = true, **options, &block) end end + # Overrides the globally configured `Content-Security-Policy-Report-Only` + # header: + # + # class PostsController < ApplicationController + # content_security_policy_report_only only: :index + # end + # + # Pass `false` to remove the `Content-Security-Policy-Report-Only` header: + # + # class PostsController < ApplicationController + # content_security_policy_report_only false, only: :index + # end def content_security_policy_report_only(report_only = true, **options) before_action(options) do request.content_security_policy_report_only = report_only diff --git a/actionpack/lib/action_controller/metal/cookies.rb b/actionpack/lib/action_controller/metal/cookies.rb index ff46966693bb0..a738532c18411 100644 --- a/actionpack/lib/action_controller/metal/cookies.rb +++ b/actionpack/lib/action_controller/metal/cookies.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true -module ActionController #:nodoc: +# :markup: markdown + +module ActionController # :nodoc: module Cookies extend ActiveSupport::Concern @@ -9,7 +11,9 @@ module Cookies end private - def cookies + # The cookies for the current request. See ActionDispatch::Cookies for more + # information. + def cookies # :doc: request.cookie_jar end end diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 879745a89568e..9c7ba594f7409 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -1,9 +1,13 @@ # frozen_string_literal: true +# :markup: markdown + require "action_controller/metal/exceptions" require "action_dispatch/http/content_disposition" -module ActionController #:nodoc: +module ActionController # :nodoc: + # # Action Controller Data Streaming + # # Methods for sending arbitrary data and for streaming files to the browser, # instead of rendering. module DataStreaming @@ -11,62 +15,66 @@ module DataStreaming include ActionController::Rendering - DEFAULT_SEND_FILE_TYPE = "application/octet-stream" #:nodoc: - DEFAULT_SEND_FILE_DISPOSITION = "attachment" #:nodoc: + DEFAULT_SEND_FILE_TYPE = "application/octet-stream" # :nodoc: + DEFAULT_SEND_FILE_DISPOSITION = "attachment" # :nodoc: private - # Sends the file. This uses a server-appropriate method (such as X-Sendfile) - # via the Rack::Sendfile middleware. The header to use is set via - # +config.action_dispatch.x_sendfile_header+. - # Your server can also configure this for you by setting the X-Sendfile-Type header. - # - # Be careful to sanitize the path parameter if it is coming from a web - # page. send_file(params[:path]) allows a malicious user to - # download any file on your server. - # - # Options: - # * :filename - suggests a filename for the browser to use. - # Defaults to File.basename(path). - # * :type - specifies an HTTP content type. - # You can specify either a string or a symbol for a registered type with Mime::Type.register, for example :json. - # If omitted, the type will be inferred from the file extension specified in :filename. - # If no content type is registered for the extension, the default type 'application/octet-stream' will be used. - # * :disposition - specifies whether the file will be shown inline or downloaded. - # Valid values are 'inline' and 'attachment' (default). - # * :status - specifies the status code to send with the response. Defaults to 200. - # * :url_based_filename - set to +true+ if you want the browser to guess the filename from - # the URL, which is necessary for i18n filenames on certain browsers - # (setting :filename overrides this option). - # - # The default Content-Type and Content-Disposition headers are - # set to download arbitrary binary files in as many browsers as - # possible. IE versions 4, 5, 5.5, and 6 are all known to have - # a variety of quirks (especially when downloading over SSL). + # Sends the file. This uses a server-appropriate method (such as `X-Sendfile`) + # via the `Rack::Sendfile` middleware. The header to use is set via + # `config.action_dispatch.x_sendfile_header`. Your server can also configure + # this for you by setting the `X-Sendfile-Type` header. + # + # Be careful to sanitize the path parameter if it is coming from a web page. + # `send_file(params[:path])` allows a malicious user to download any file on + # your server. + # + # #### Options: + # + # * `:filename` - suggests a filename for the browser to use. Defaults to + # `File.basename(path)`. + # * `:type` - specifies an HTTP content type. You can specify either a string + # or a symbol for a registered type with `Mime::Type.register`, for example + # `:json`. If omitted, the type will be inferred from the file extension + # specified in `:filename`. If no content type is registered for the + # extension, the default type `application/octet-stream` will be used. + # * `:disposition` - specifies whether the file will be shown inline or + # downloaded. Valid values are `"inline"` and `"attachment"` (default). + # * `:status` - specifies the status code to send with the response. Defaults + # to 200. + # * `:url_based_filename` - set to `true` if you want the browser to guess the + # filename from the URL, which is necessary for i18n filenames on certain + # browsers (setting `:filename` overrides this option). + # + # + # The default `Content-Type` and `Content-Disposition` headers are set to + # download arbitrary binary files in as many browsers as possible. IE versions + # 4, 5, 5.5, and 6 are all known to have a variety of quirks (especially when + # downloading over SSL). # # Simple download: # - # send_file '/path/to.zip' + # send_file '/path/to.zip' # # Show a JPEG in the browser: # - # send_file '/path/to.jpeg', type: 'image/jpeg', disposition: 'inline' + # send_file '/path/to.jpeg', type: 'image/jpeg', disposition: 'inline' # # Show a 404 page in the browser: # - # send_file '/path/to/404.html', type: 'text/html; charset=utf-8', disposition: 'inline', status: 404 + # send_file '/path/to/404.html', type: 'text/html; charset=utf-8', disposition: 'inline', status: 404 # - # Read about the other Content-* HTTP headers if you'd like to - # provide the user with more information (such as Content-Description) in - # https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11. + # You can use other `Content-*` HTTP headers to provide additional information + # to the client. See MDN for a [list of HTTP + # headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers). # - # Also be aware that the document may be cached by proxies and browsers. - # The Pragma and Cache-Control headers declare how the file may be cached - # by intermediaries. They default to require clients to validate with - # the server before releasing cached responses. See - # https://www.mnot.net/cache_docs/ for an overview of web caching and - # https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 - # for the Cache-Control header spec. - def send_file(path, options = {}) #:doc: + # Also be aware that the document may be cached by proxies and browsers. The + # `Pragma` and `Cache-Control` headers declare how the file may be cached by + # intermediaries. They default to require clients to validate with the server + # before releasing cached responses. See https://www.mnot.net/cache_docs/ for an + # overview of web caching and [RFC + # 9111](https://www.rfc-editor.org/rfc/rfc9111.html#name-cache-control) for the + # `Cache-Control` header spec. + def send_file(path, options = {}) # :doc: raise MissingFile, "Cannot read file #{path}" unless File.file?(path) && File.readable?(path) options[:filename] ||= File.basename(path) unless options[:url_based_filename] @@ -77,36 +85,41 @@ def send_file(path, options = {}) #:doc: response.send_file path end - # Sends the given binary data to the browser. This method is similar to - # render plain: data, but also allows you to specify whether - # the browser should display the response as a file attachment (i.e. in a - # download dialog) or as inline data. You may also set the content type, - # the file name, and other things. - # - # Options: - # * :filename - suggests a filename for the browser to use. - # * :type - specifies an HTTP content type. Defaults to 'application/octet-stream'. - # You can specify either a string or a symbol for a registered type with Mime::Type.register, for example :json. - # If omitted, type will be inferred from the file extension specified in :filename. - # If no content type is registered for the extension, the default type 'application/octet-stream' will be used. - # * :disposition - specifies whether the file will be shown inline or downloaded. - # Valid values are 'inline' and 'attachment' (default). - # * :status - specifies the status code to send with the response. Defaults to 200. + # Sends the given binary data to the browser. This method is similar to `render + # plain: data`, but also allows you to specify whether the browser should + # display the response as a file attachment (i.e. in a download dialog) or as + # inline data. You may also set the content type, the file name, and other + # things. + # + # #### Options: + # + # * `:filename` - suggests a filename for the browser to use. + # * `:type` - specifies an HTTP content type. Defaults to + # `application/octet-stream`. You can specify either a string or a symbol + # for a registered type with `Mime::Type.register`, for example `:json`. If + # omitted, type will be inferred from the file extension specified in + # `:filename`. If no content type is registered for the extension, the + # default type `application/octet-stream` will be used. + # * `:disposition` - specifies whether the file will be shown inline or + # downloaded. Valid values are `"inline"` and `"attachment"` (default). + # * `:status` - specifies the status code to send with the response. Defaults + # to 200. + # # # Generic data download: # - # send_data buffer + # send_data buffer # # Download a dynamically-generated tarball: # - # send_data generate_tgz('dir'), filename: 'dir.tgz' + # send_data generate_tgz('dir'), filename: 'dir.tgz' # # Display an image Active Record in the browser: # - # send_data image.data, type: image.content_type, disposition: 'inline' + # send_data image.data, type: image.content_type, disposition: 'inline' # - # See +send_file+ for more information on HTTP Content-* headers and caching. - def send_data(data, options = {}) #:doc: + # See `send_file` for more information on HTTP `Content-*` headers and caching. + def send_data(data, options = {}) # :doc: send_file_headers! options render options.slice(:status, :content_type).merge(body: data) end @@ -121,9 +134,7 @@ def send_file_headers!(options) raise ArgumentError, ":type option required" if content_type.nil? if content_type.is_a?(Symbol) - extension = Mime[content_type] - raise ArgumentError, "Unknown MIME type #{options[:type]}" unless extension - self.content_type = extension + self.content_type = content_type else if !type_provided && options[:filename] # If type wasn't provided, try guessing from file extension. @@ -138,14 +149,6 @@ def send_file_headers!(options) end headers["Content-Transfer-Encoding"] = "binary" - - # Fix a problem with IE 6.0 on opening downloaded files: - # If Cache-Control: no-cache is set (which Rails does by default), - # IE removes the file it just downloaded from its cache immediately - # after it displays the "open/save" dialog, which means that if you - # hit "open" the file isn't there anymore when the application that - # is called for handling the download is run, so let's workaround that - response.cache_control[:public] ||= false end end end diff --git a/actionpack/lib/action_controller/metal/default_headers.rb b/actionpack/lib/action_controller/metal/default_headers.rb index eef0602fcdde1..fef8e789467ef 100644 --- a/actionpack/lib/action_controller/metal/default_headers.rb +++ b/actionpack/lib/action_controller/metal/default_headers.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true +# :markup: markdown + module ActionController - # Allows configuring default headers that will be automatically merged into - # each response. + # # Action Controller Default Headers + # + # Allows configuring default headers that will be automatically merged into each + # response. module DefaultHeaders extend ActiveSupport::Concern diff --git a/actionpack/lib/action_controller/metal/etag_with_flash.rb b/actionpack/lib/action_controller/metal/etag_with_flash.rb index 38899e2f160ce..6caf8a4315323 100644 --- a/actionpack/lib/action_controller/metal/etag_with_flash.rb +++ b/actionpack/lib/action_controller/metal/etag_with_flash.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true +# :markup: markdown + module ActionController + # # Action Controller Etag With Flash + # # When you're using the flash, it's generally used as a conditional on the view. # This means the content of the view depends on the flash. Which in turn means # that the ETag for a response should be computed with the content of the flash @@ -12,7 +16,7 @@ module EtagWithFlash include ActionController::ConditionalGet included do - etag { flash unless flash.empty? } + etag { flash if request.respond_to?(:flash) && !flash.empty? } end end end diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb index 983bd745bd7c2..20715b1fc6c56 100644 --- a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb +++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb @@ -1,22 +1,26 @@ # frozen_string_literal: true +# :markup: markdown + module ActionController - # When our views change, they should bubble up into HTTP cache freshness - # and bust browser caches. So the template digest for the current action - # is automatically included in the ETag. + # # Action Controller Etag With Template Digest + # + # When our views change, they should bubble up into HTTP cache freshness and + # bust browser caches. So the template digest for the current action is + # automatically included in the ETag. # # Enabled by default for apps that use Action View. Disable by setting # - # config.action_controller.etag_with_template_digest = false + # config.action_controller.etag_with_template_digest = false # - # Override the template to digest by passing +:template+ to +fresh_when+ - # and +stale?+ calls. For example: + # Override the template to digest by passing `:template` to `fresh_when` and + # `stale?` calls. For example: # - # # We're going to render widgets/show, not posts/show - # fresh_when @post, template: 'widgets/show' + # # We're going to render widgets/show, not posts/show + # fresh_when @post, template: 'widgets/show' # - # # We're not going to render a template, so omit it from the ETag. - # fresh_when @post, template: false + # # We're not going to render a template, so omit it from the ETag. + # fresh_when @post, template: false # module EtagWithTemplateDigest extend ActiveSupport::Concern @@ -38,13 +42,13 @@ def determine_template_etag(options) end end - # Pick the template digest to include in the ETag. If the +:template+ option - # is present, use the named template. If +:template+ is +nil+ or absent, use - # the default controller/action template. If +:template+ is false, omit the - # template digest from the ETag. + # Pick the template digest to include in the ETag. If the `:template` option is + # present, use the named template. If `:template` is `nil` or absent, use the + # default controller/action template. If `:template` is false, omit the template + # digest from the ETag. def pick_template_for_etag(options) unless options[:template] == false - options[:template] || "#{controller_path}/#{action_name}" + options[:template] || lookup_context.find_all(action_name, _prefixes).first&.virtual_path end end diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index 7f10b8c869c77..35dd1a9138eaf 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -1,20 +1,22 @@ # frozen_string_literal: true +# :markup: markdown + module ActionController - class ActionControllerError < StandardError #:nodoc: + class ActionControllerError < StandardError # :nodoc: end - class BadRequest < ActionControllerError #:nodoc: + class BadRequest < ActionControllerError # :nodoc: def initialize(msg = nil) super(msg) set_backtrace $!.backtrace if $! end end - class RenderError < ActionControllerError #:nodoc: + class RenderError < ActionControllerError # :nodoc: end - class RoutingError < ActionControllerError #:nodoc: + class RoutingError < ActionControllerError # :nodoc: attr_reader :failures def initialize(message, failures = []) super(message) @@ -22,22 +24,44 @@ def initialize(message, failures = []) end end - class UrlGenerationError < ActionControllerError #:nodoc: + class UrlGenerationError < ActionControllerError # :nodoc: + attr_reader :routes, :route_name, :method_name + + def initialize(message, routes = nil, route_name = nil, method_name = nil) + @routes = routes + @route_name = route_name + @method_name = method_name + + super(message) + end + + if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker) + include DidYouMean::Correctable + + def corrections + @corrections ||= begin + maybe_these = routes&.named_routes&.helper_names&.grep(/#{route_name}/) || [] + maybe_these -= [method_name.to_s] # remove exact match + + DidYouMean::SpellChecker.new(dictionary: maybe_these).correct(route_name) + end + end + end end - class MethodNotAllowed < ActionControllerError #:nodoc: + class MethodNotAllowed < ActionControllerError # :nodoc: def initialize(*allowed_methods) super("Only #{allowed_methods.to_sentence} requests are allowed.") end end - class NotImplemented < MethodNotAllowed #:nodoc: + class NotImplemented < MethodNotAllowed # :nodoc: end - class MissingFile < ActionControllerError #:nodoc: + class MissingFile < ActionControllerError # :nodoc: end - class SessionOverflowError < ActionControllerError #:nodoc: + class SessionOverflowError < ActionControllerError # :nodoc: DEFAULT_MESSAGE = "Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data." def initialize(message = nil) @@ -45,22 +69,22 @@ def initialize(message = nil) end end - class UnknownHttpMethod < ActionControllerError #:nodoc: + class UnknownHttpMethod < ActionControllerError # :nodoc: end - class UnknownFormat < ActionControllerError #:nodoc: + class UnknownFormat < ActionControllerError # :nodoc: end - # Raised when a nested respond_to is triggered and the content types of each - # are incompatible. For example: + # Raised when a nested respond_to is triggered and the content types of each are + # incompatible. For example: # - # respond_to do |outer_type| - # outer_type.js do - # respond_to do |inner_type| - # inner_type.html { render body: "HTML" } - # end - # end - # end + # respond_to do |outer_type| + # outer_type.js do + # respond_to do |inner_type| + # inner_type.html { render body: "HTML" } + # end + # end + # end class RespondToMismatchError < ActionControllerError DEFAULT_MESSAGE = "respond_to was called multiple times and matched with conflicting formats in this action. Please note that you may only call respond_to and match on a single format per action." @@ -69,6 +93,14 @@ def initialize(message = nil) end end - class MissingExactTemplate < UnknownFormat #:nodoc: + class MissingExactTemplate < UnknownFormat # :nodoc: + attr_reader :controller, :action_name + + def initialize(message, controller, action_name) + @controller = controller + @action_name = action_name + + super(message) + end end end diff --git a/actionpack/lib/action_controller/metal/feature_policy.rb b/actionpack/lib/action_controller/metal/feature_policy.rb deleted file mode 100644 index a627eabea615b..0000000000000 --- a/actionpack/lib/action_controller/metal/feature_policy.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module ActionController #:nodoc: - # HTTP Feature Policy is a web standard for defining a mechanism to - # allow and deny the use of browser features in its own context, and - # in content within any