From 186e9ae2bc05e76f2201daf1de219c1d297f53ca Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Mon, 11 Nov 2024 15:07:49 +0100 Subject: [PATCH] [CHORE] Update Kamal, fix deployment, update some gems and minor things --- .gitignore | 1 + .kamal/hooks/post-deploy | 9 ++ Dockerfile | 17 +-- Gemfile | 20 +-- Gemfile.lock | 201 +++++++++++++------------- bin/thrust | 5 + config/credentials/production.yml.enc | 2 +- config/deploy.yml | 42 ++---- config/environments/production.rb | 4 +- config/vite.json | 6 +- vite.config.mjs | 4 + 11 files changed, 161 insertions(+), 150 deletions(-) create mode 100755 .kamal/hooks/post-deploy create mode 100755 bin/thrust diff --git a/.gitignore b/.gitignore index fbc18c7..8e2ed20 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ node_modules llm /config/credentials/test.key +.kamal/secrets diff --git a/.kamal/hooks/post-deploy b/.kamal/hooks/post-deploy new file mode 100755 index 0000000..55ffe5c --- /dev/null +++ b/.kamal/hooks/post-deploy @@ -0,0 +1,9 @@ +#!/bin/bash + +echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" + +echo "Running migrations" + +kamal bundle exec rails db:migrate +kamal bundle exec rails db:migrate:queue +kamal bundle exec rails db:migrate:cache diff --git a/Dockerfile b/Dockerfile index 9947667..c31535a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,9 @@ WORKDIR /rails # Install base packages RUN apt-get update -qq && \ apt-get install --no-install-recommends -y curl libjemalloc2 libsqlite3-0 \ - build-essential libssl-dev git pkg-config python-is-python3 libgmp-dev ca-certificates gnupg xz-utils && \ + build-essential libssl-dev git pkg-config python-is-python3 libgmp-dev ca-certificates gnupg xz-utils \ + libffi-dev libyaml-dev libreadline-dev zlib1g-dev libncurses5-dev libgdbm-dev \ + libc6-dev && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Set production environment @@ -40,7 +42,8 @@ FROM base AS build # Install application gems COPY Gemfile Gemfile.lock ./ RUN bundle config set --local build.nokogiri --use-system-libraries && \ - bundle install --jobs 4 --retry 3 && \ + bundle config build.msgpack --with-cflags="-O2" && \ + bundle install --jobs 4 --retry 5 && \ rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git # Install node modules @@ -56,8 +59,6 @@ RUN bundle exec bootsnap precompile app/ lib/ # Precompiling assets for production without requiring secret RAILS_MASTER_KEY RUN RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile -# RUN yarn vite build - # Final stage for app image FROM base @@ -78,9 +79,5 @@ USER rails # Entrypoint prepares the database. ENTRYPOINT ["/rails/bin/docker-entrypoint"] -ENV HTTP_PORT="3000" \ - TARGET_PORT="3001" - -# Start the server by default, this can be overwritten at runtime -EXPOSE 3000 -CMD ["bundle", "exec", "thrust", "./bin/rails", "server"] +EXPOSE 80 +CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/Gemfile b/Gemfile index 398f6d9..ededa2d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,22 +1,22 @@ source "https://rubygems.org" -gem "rails", "8.0.0.beta1" +gem "rails", "8.0.0.rc2" gem "bootsnap", require: false gem "friendly_id", "~> 5.5.1" -gem "kamal", "~> 1.8.3", require: false -gem "mission_control-jobs", "~> 0.3.3" -gem "litestream", "~> 0.11.2" +gem "kamal", "~> 2.3.0", require: false +gem "thruster", "~> 0.1.8", require: false +gem "mission_control-jobs", "~> 0.4.0" +gem "litestream", "~> 0.12.0" gem "propshaft", "~> 1.1.0" gem "solid_cache", "~> 1.0.6" -gem "solid_queue", "~> 1.0.0" -gem "sqlite3", "~> 2.1.0" +gem "solid_queue", "~> 1.0.1" +gem "sqlite3", "~> 2.2.0" gem "stimulus-rails" gem "turbo-rails", "~> 2.0.11" gem "puma", ">= 6.4.3" gem "phlex-rails", "~> 1.2.1" -gem "thruster", "~> 0.1.8" -gem "vite_rails", "~> 3.0.17" +gem "vite_rails", "~> 3.0.19" group :development do gem "annotaterb" @@ -28,8 +28,8 @@ end group :test do gem "capybara" - gem "selenium-webdriver", "~> 4.25.0" - gem "mocha", "~> 2.4.5" + gem "selenium-webdriver", "~> 4.26.0" + gem "mocha", "~> 2.5.0" gem "simplecov", require: false gem "simplecov-tailwindcss", require: false end diff --git a/Gemfile.lock b/Gemfile.lock index fe2be13..742f187 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,29 @@ GEM remote: https://rubygems.org/ specs: - actioncable (8.0.0.beta1) - actionpack (= 8.0.0.beta1) - activesupport (= 8.0.0.beta1) + actioncable (8.0.0.rc2) + actionpack (= 8.0.0.rc2) + activesupport (= 8.0.0.rc2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.0.0.beta1) - actionpack (= 8.0.0.beta1) - activejob (= 8.0.0.beta1) - activerecord (= 8.0.0.beta1) - activestorage (= 8.0.0.beta1) - activesupport (= 8.0.0.beta1) + actionmailbox (8.0.0.rc2) + actionpack (= 8.0.0.rc2) + activejob (= 8.0.0.rc2) + activerecord (= 8.0.0.rc2) + activestorage (= 8.0.0.rc2) + activesupport (= 8.0.0.rc2) mail (>= 2.8.0) - actionmailer (8.0.0.beta1) - actionpack (= 8.0.0.beta1) - actionview (= 8.0.0.beta1) - activejob (= 8.0.0.beta1) - activesupport (= 8.0.0.beta1) + actionmailer (8.0.0.rc2) + actionpack (= 8.0.0.rc2) + actionview (= 8.0.0.rc2) + activejob (= 8.0.0.rc2) + activesupport (= 8.0.0.rc2) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.0.0.beta1) - actionview (= 8.0.0.beta1) - activesupport (= 8.0.0.beta1) + actionpack (8.0.0.rc2) + actionview (= 8.0.0.rc2) + activesupport (= 8.0.0.rc2) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -31,35 +31,35 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.0.0.beta1) - actionpack (= 8.0.0.beta1) - activerecord (= 8.0.0.beta1) - activestorage (= 8.0.0.beta1) - activesupport (= 8.0.0.beta1) + actiontext (8.0.0.rc2) + actionpack (= 8.0.0.rc2) + activerecord (= 8.0.0.rc2) + activestorage (= 8.0.0.rc2) + activesupport (= 8.0.0.rc2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.0.beta1) - activesupport (= 8.0.0.beta1) + actionview (8.0.0.rc2) + activesupport (= 8.0.0.rc2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.0.0.beta1) - activesupport (= 8.0.0.beta1) + activejob (8.0.0.rc2) + activesupport (= 8.0.0.rc2) globalid (>= 0.3.6) - activemodel (8.0.0.beta1) - activesupport (= 8.0.0.beta1) - activerecord (8.0.0.beta1) - activemodel (= 8.0.0.beta1) - activesupport (= 8.0.0.beta1) + activemodel (8.0.0.rc2) + activesupport (= 8.0.0.rc2) + activerecord (8.0.0.rc2) + activemodel (= 8.0.0.rc2) + activesupport (= 8.0.0.rc2) timeout (>= 0.4.0) - activestorage (8.0.0.beta1) - actionpack (= 8.0.0.beta1) - activejob (= 8.0.0.beta1) - activerecord (= 8.0.0.beta1) - activesupport (= 8.0.0.beta1) + activestorage (8.0.0.rc2) + actionpack (= 8.0.0.rc2) + activejob (= 8.0.0.rc2) + activerecord (= 8.0.0.rc2) + activesupport (= 8.0.0.rc2) marcel (~> 1.0) - activesupport (8.0.0.beta1) + activesupport (8.0.0.rc2) base64 benchmark (>= 0.3) bigdecimal @@ -110,17 +110,17 @@ GEM concurrent-ruby (1.3.4) connection_pool (2.4.1) crass (1.0.6) - date (3.3.4) + date (3.4.0) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) docile (1.4.1) - dotenv (2.8.1) - dotenv-rails (2.8.1) - dotenv (= 2.8.1) - railties (>= 3.2) + dotenv (3.1.4) + dotenv-rails (3.1.4) + dotenv (= 3.1.4) + railties (>= 6.1) drb (2.2.1) - dry-cli (1.1.0) + dry-cli (1.2.0) ed25519 (1.3.0) erb_lint (0.6.0) activesupport @@ -151,19 +151,19 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.7.2) - kamal (1.8.3) + kamal (2.3.0) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) concurrent-ruby (~> 1.2) - dotenv (~> 2.8) + dotenv (~> 3.1) ed25519 (~> 1.2) - net-ssh (~> 7.0) + net-ssh (~> 7.3) sshkit (>= 1.23.0, < 2.0) - thor (~> 1.2) - zeitwerk (~> 2.5) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) language_server-protocol (3.17.0.3) - litestream (0.11.2) + litestream (0.12.0) actionpack (>= 7.0) actionview (>= 7.0) activejob (>= 7.0) @@ -171,7 +171,7 @@ GEM logfmt (>= 0.0.10) railties (>= 7.0) sqlite3 - litestream (0.11.2-arm64-darwin) + litestream (0.12.0-arm64-darwin) actionpack (>= 7.0) actionview (>= 7.0) activejob (>= 7.0) @@ -179,7 +179,7 @@ GEM logfmt (>= 0.0.10) railties (>= 7.0) sqlite3 - litestream (0.11.2-x86_64-darwin) + litestream (0.12.0-x86_64-darwin) actionpack (>= 7.0) actionview (>= 7.0) activejob (>= 7.0) @@ -187,7 +187,7 @@ GEM logfmt (>= 0.0.10) railties (>= 7.0) sqlite3 - litestream (0.11.2-x86_64-linux) + litestream (0.12.0-x86_64-linux) actionpack (>= 7.0) actionview (>= 7.0) activejob (>= 7.0) @@ -214,16 +214,20 @@ GEM minio (0.4.0-x86_64-darwin) minio (0.4.0-x86_64-linux) minitest (5.25.1) - mission_control-jobs (0.3.3) - importmap-rails + mission_control-jobs (0.4.0) + actioncable (>= 7.1) + actionpack (>= 7.1) + activejob (>= 7.1) + activerecord (>= 7.1) + importmap-rails (>= 1.2.1) irb (~> 1.13) - rails (>= 7.1) + railties (>= 7.1) stimulus-rails turbo-rails - mocha (2.4.5) + mocha (2.5.0) ruby2_keywords (>= 0.0.5) msgpack (1.7.2) - net-imap (0.4.16) + net-imap (0.5.0) date net-protocol net-pop (0.1.2) @@ -285,20 +289,20 @@ GEM rackup (2.1.0) rack (>= 3) webrick (~> 1.8) - rails (8.0.0.beta1) - actioncable (= 8.0.0.beta1) - actionmailbox (= 8.0.0.beta1) - actionmailer (= 8.0.0.beta1) - actionpack (= 8.0.0.beta1) - actiontext (= 8.0.0.beta1) - actionview (= 8.0.0.beta1) - activejob (= 8.0.0.beta1) - activemodel (= 8.0.0.beta1) - activerecord (= 8.0.0.beta1) - activestorage (= 8.0.0.beta1) - activesupport (= 8.0.0.beta1) + rails (8.0.0.rc2) + actioncable (= 8.0.0.rc2) + actionmailbox (= 8.0.0.rc2) + actionmailer (= 8.0.0.rc2) + actionpack (= 8.0.0.rc2) + actiontext (= 8.0.0.rc2) + actionview (= 8.0.0.rc2) + activejob (= 8.0.0.rc2) + activemodel (= 8.0.0.rc2) + activerecord (= 8.0.0.rc2) + activestorage (= 8.0.0.rc2) + activesupport (= 8.0.0.rc2) bundler (>= 1.15.0) - railties (= 8.0.0.beta1) + railties (= 8.0.0.rc2) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -306,9 +310,9 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (8.0.0.beta1) - actionpack (= 8.0.0.beta1) - activesupport (= 8.0.0.beta1) + railties (8.0.0.rc2) + actionpack (= 8.0.0.rc2) + activesupport (= 8.0.0.rc2) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -354,7 +358,7 @@ GEM ruby2_keywords (0.0.5) rubyzip (2.3.2) securerandom (0.3.1) - selenium-webdriver (4.25.0) + selenium-webdriver (4.26.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) @@ -373,24 +377,24 @@ GEM activejob (>= 7.2) activerecord (>= 7.2) railties (>= 7.2) - solid_queue (1.0.0) + solid_queue (1.0.1) activejob (>= 7.1) activerecord (>= 7.1) concurrent-ruby (>= 1.3.1) fugit (~> 1.11.0) railties (>= 7.1) thor (~> 1.3.1) - sqlite3 (2.1.0-aarch64-linux-gnu) - sqlite3 (2.1.0-aarch64-linux-musl) - sqlite3 (2.1.0-arm-linux-gnu) - sqlite3 (2.1.0-arm-linux-musl) - sqlite3 (2.1.0-arm64-darwin) - sqlite3 (2.1.0-x86-linux-gnu) - sqlite3 (2.1.0-x86-linux-musl) - sqlite3 (2.1.0-x86_64-darwin) - sqlite3 (2.1.0-x86_64-linux-gnu) - sqlite3 (2.1.0-x86_64-linux-musl) - sshkit (1.23.1) + sqlite3 (2.2.0-aarch64-linux-gnu) + sqlite3 (2.2.0-aarch64-linux-musl) + sqlite3 (2.2.0-arm-linux-gnu) + sqlite3 (2.2.0-arm-linux-musl) + sqlite3 (2.2.0-arm64-darwin) + sqlite3 (2.2.0-x86-linux-gnu) + sqlite3 (2.2.0-x86-linux-musl) + sqlite3 (2.2.0-x86_64-darwin) + sqlite3 (2.2.0-x86_64-linux-gnu) + sqlite3 (2.2.0-x86_64-linux-musl) + sshkit (1.23.2) base64 net-scp (>= 1.1.2) net-sftp (>= 2.1.2) @@ -414,11 +418,12 @@ GEM unicode-display_width (2.6.0) uri (0.13.1) useragent (0.16.10) - vite_rails (3.0.17) - railties (>= 5.1, < 8) + vite_rails (3.0.19) + railties (>= 5.1, < 9) vite_ruby (~> 3.0, >= 3.2.2) - vite_ruby (3.8.2) + vite_ruby (3.9.0) dry-cli (>= 0.7, < 2) + logger (~> 1.6) rack-proxy (~> 0.6, >= 0.6.1) zeitwerk (~> 2.2) web-console (4.2.1) @@ -462,27 +467,27 @@ DEPENDENCIES dotenv-rails erb_lint friendly_id (~> 5.5.1) - kamal (~> 1.8.3) - litestream (~> 0.11.2) + kamal (~> 2.3.0) + litestream (~> 0.12.0) minio (~> 0.4.0) - mission_control-jobs (~> 0.3.3) - mocha (~> 2.4.5) + mission_control-jobs (~> 0.4.0) + mocha (~> 2.5.0) overcommit phlex-rails (~> 1.2.1) propshaft (~> 1.1.0) puma (>= 6.4.3) - rails (= 8.0.0.beta1) + rails (= 8.0.0.rc2) rubocop-rails-omakase - selenium-webdriver (~> 4.25.0) + selenium-webdriver (~> 4.26.0) simplecov simplecov-tailwindcss solid_cache (~> 1.0.6) - solid_queue (~> 1.0.0) - sqlite3 (~> 2.1.0) + solid_queue (~> 1.0.1) + sqlite3 (~> 2.2.0) stimulus-rails thruster (~> 0.1.8) turbo-rails (~> 2.0.11) - vite_rails (~> 3.0.17) + vite_rails (~> 3.0.19) web-console BUNDLED WITH diff --git a/bin/thrust b/bin/thrust new file mode 100755 index 0000000..36bde2d --- /dev/null +++ b/bin/thrust @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("thruster", "thrust") diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc index 7acc9b3..796a3bb 100644 --- a/config/credentials/production.yml.enc +++ b/config/credentials/production.yml.enc @@ -1 +1 @@ -4MBI109YKFvR/bJ53ER5aGLmFE4UOq2I//d3JruvVsnmtC+N2Ee9mUJqQoV20nEWh0P0xJNzZzk/oaB+5RoTHUsn+HG3/yNDPFhKC3fV09RVSHGc6hGghcWniPVwZeULUvrJr2NNztcMPONmkltrsi+l/cu+OcDY8p8Thcivjp83X8C7J2aVU88W+hm72DrCo4/FZbOpsUb2liAi57b8pkVvev3T9OnEjLa79EdVRbJjoWCjqIIGhPkX9lSoycqcc7c3SkW5bDiJiVO9eHNiCFO8/+0BG5XQumSK82COT3355A9iaWAqx+jH--7Bk0m+nd1jihQBP4--1sFJBMHwNzSiUL4nWpGjrQ== \ No newline at end of file +3EpzulcVbSg2dq4L55l7wpMG9ULgcjdHHeVrFWb7o0barxucpvnPorDssULfEzQKOmPReR1jv5LsnONxBMOLft7Pb+Msyq6svPVyN8teRJT6ClU0IutLy3fyLAG3UygKifYhgTqdsMk5JAqdN6s00VNreSGmDXMzEz6BOGZIrH8PshggYRKPC5AxguGt78VYf6dgbt3rMQlCW/2+V8QgrJsYqfDljjABeQjNRtXsI/yHUNfd6cOTbfALENUK1wYcQp5BKKu/oVHhsN19d3g1yh0tJPyT4OLAocwfEtgR6WCeheVTQf1bocb0VR0a8Y0cvGCEd3VXbKDhEspCUJBw48BDxuNcOS010fd9L8mJDnOvdcZBordo9aYD0Y+7YqhmvifiGDfOAO0wdq4B97E7QNn2aIbJ7d2bOQ==--z1N/dGCYvtprXCZe--5jhVeD5X/C989OcQF6KE0w== \ No newline at end of file diff --git a/config/deploy.yml b/config/deploy.yml index 9f39146..c38f22c 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -11,12 +11,6 @@ servers: web: hosts: - 5.161.247.143 - labels: - traefik.http.routers.rails_new_io.rule: Host(`alpha.railsnew.io`) - traefik.http.routers.rails_new_io_secure.entrypoints: websecure - traefik.http.routers.rails_new_io_secure.rule: Host(`alpha.railsnew.io`) - traefik.http.routers.rails_new_io_secure.tls: true - traefik.http.routers.rails_new_io_secure.tls.certresolver: letsencrypt options: network: "private" job: @@ -26,21 +20,6 @@ servers: options: network: "private" -traefik: - options: - publish: - - "443:443" - volume: - - "/letsencrypt/acme.json:/letsencrypt/acme.json" - network: "private" - args: - entryPoints.web.address: ":80" - entryPoints.websecure.address: ":443" - certificatesResolvers.letsencrypt.acme.email: "trinity@railsnew.io" - certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json" - certificatesResolvers.letsencrypt.acme.httpchallenge: true - certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web - registry: server: ghcr.io username: trinitytakei @@ -48,7 +27,9 @@ registry: - KAMAL_REGISTRY_PASSWORD builder: - multiarch: <%= ENV["CI"].present? ? false : true %> + arch: + - amd64 + - arm64 cache: type: registry image: trinitytakei/rails-new-io-build-cache @@ -56,10 +37,19 @@ builder: env: clear: - HOST: alpha.railsnew.io - PORT: 3001 - RAILS_SERVE_STATIC_FILES: true - RAILS_LOG_TO_STDOUT: true SKIP_COVERAGE: "1" + RAILS_SERVE_STATIC_FILES: true + RAILS_LOG_LEVEL: debug + SOLID_QUEUE_IN_PUMA: true secret: - RAILS_MASTER_KEY + +proxy: + ssl: true + host: alpha.railsnew.io + +aliases: + console: app exec --interactive --reuse "bin/rails console" + shell: app exec --interactive --reuse "bash" + logs: app logs -f + dbc: app exec --interactive --reuse "bin/rails dbconsole" diff --git a/config/environments/production.rb b/config/environments/production.rb index 12ef0d0..cb2cd6a 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -45,7 +45,7 @@ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true # Skip http-to-https redirect for the default health check endpoint. - # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } # Log to STDOUT by default config.logger = ActiveSupport::Logger.new(STDOUT) @@ -91,5 +91,5 @@ # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` # ] # Skip DNS rebinding protection for the default health check endpoint. - # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } + config.host_authorization = { exclude: ->(request) { request.path == "/up" } } end diff --git a/config/vite.json b/config/vite.json index 04f2741..f90fe57 100644 --- a/config/vite.json +++ b/config/vite.json @@ -3,7 +3,7 @@ "sourceCodeDir": "app/javascript", "watchAdditionalPaths": [], "entrypoints": { - "application": ["application.js", "application.css"] + "application": ["entrypoints/application.js"] } }, "development": { @@ -17,7 +17,7 @@ "port": 3037 }, "production": { - "autoBuild": true, - "publicOutputDir": "vite" + "autoBuild": false, + "publicOutputDir": "public" } } diff --git a/vite.config.mjs b/vite.config.mjs index d7cfd79..966e0f7 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -19,4 +19,8 @@ export default defineConfig({ css: { postcss: './postcss.config.cjs', }, + build: { + manifest: true, + outDir: '.', + }, });