diff --git a/app/controllers/erdbeere_controller.rb b/app/controllers/erdbeere_controller.rb index 23c78601e..f47ec1daa 100644 --- a/app/controllers/erdbeere_controller.rb +++ b/app/controllers/erdbeere_controller.rb @@ -8,7 +8,7 @@ def current_ability end def show_example - response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) + "/examples/#{params[:id]}") + response = Faraday.get(ENV.fetch("ERDBEERE_API") + "/examples/#{params[:id]}") @content = if response.status == 200 JSON.parse(response.body)["embedded_html"] else @@ -17,7 +17,7 @@ def show_example end def show_property - response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) + "/properties/#{params[:id]}") + response = Faraday.get(ENV.fetch("ERDBEERE_API") + "/properties/#{params[:id]}") @content = if response.status == 200 JSON.parse(response.body)["embedded_html"] @@ -28,7 +28,7 @@ def show_property def show_structure params[:id] - response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) + "/structures/#{params[:id]}") + response = Faraday.get(ENV.fetch("ERDBEERE_API") + "/structures/#{params[:id]}") @content = if response.status == 200 JSON.parse(response.body)["embedded_html"] else @@ -51,7 +51,7 @@ def cancel_edit_tags def display_info @id = params[:id] @sort = params[:sort] - response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) + + response = Faraday.get(ENV.fetch("ERDBEERE_API") + "/#{@sort.downcase.pluralize}/#{@id}/view_info") @content = JSON.parse(response.body) if response.status != 200 @@ -87,7 +87,7 @@ def update_tags end def fill_realizations_select - response = Faraday.get("#{ENV.fetch("ERDBEERE_API", nil)}/structures/") + response = Faraday.get("#{ENV.fetch("ERDBEERE_API")}/structures/") @tag = Tag.find_by(id: params[:id]) hash = JSON.parse(response.body) @structures = hash["data"].map do |d| @@ -102,7 +102,7 @@ def fill_realizations_select end def find_example - response = Faraday.get("#{ENV.fetch("ERDBEERE_API", nil)}/find?#{find_params.to_query}") + response = Faraday.get("#{ENV.fetch("ERDBEERE_API")}/find?#{find_params.to_query}") @content = if response.status == 200 JSON.parse(response.body)["embedded_html"] else diff --git a/app/controllers/lectures_controller.rb b/app/controllers/lectures_controller.rb index d108fbbc0..a7196ea79 100644 --- a/app/controllers/lectures_controller.rb +++ b/app/controllers/lectures_controller.rb @@ -217,7 +217,7 @@ def edit_structures def search_examples if @lecture.structure_ids.any? - response = Faraday.get("#{ENV.fetch("ERDBEERE_API", nil)}/search") + response = Faraday.get("#{ENV.fetch("ERDBEERE_API")}/search") @form = JSON.parse(response.body)["embedded_html"] # rubocop:disable Style/StringConcatenation @form.gsub!("token_placeholder", @@ -402,7 +402,7 @@ def eager_load_stuff def set_erdbeere_data @structure_ids = @lecture.structure_ids - response = Faraday.get("#{ENV.fetch("ERDBEERE_API", nil)}/structures") + response = Faraday.get("#{ENV.fetch("ERDBEERE_API")}/structures") response_hash = if response.status == 200 JSON.parse(response.body) else diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index bc9152b78..939f2d56b 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -9,12 +9,12 @@ def verify_captcha return true unless ENV["USE_CAPTCHA_SERVICE"] begin - uri = URI.parse(ENV.fetch("CAPTCHA_VERIFY_URL", nil)) + uri = URI.parse(ENV.fetch("CAPTCHA_VERIFY_URL")) data = { message: params["frc-captcha-solution"], - application_token: ENV.fetch("CAPTCHA_APPLICATION_TOKEN", nil) } + application_token: ENV.fetch("CAPTCHA_APPLICATION_TOKEN") } header = { "Content-Type": "text/json" } http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true if ENV["CAPTCHA_VERIFY_URL"].include?("https") + http.use_ssl = true if ENV.fetch("CAPTCHA_VERIFY_URL").include?("https") request = Net::HTTP::Post.new(uri.request_uri, header) request.body = data.to_json @@ -70,9 +70,9 @@ def after_sign_up_path_for(_resource) private def check_registration_limit - timeframe = ((ENV["MAMPF_REGISTRATION_TIMEFRAME"] || 15).to_i.minutes.ago..) + timeframe = (ENV.fetch("MAMPF_REGISTRATION_TIMEFRAME", 15).to_i.minutes.ago..) num_new_registrations = User.where(confirmed_at: nil, created_at: timeframe).count - max_registrations = (ENV["MAMPF_MAX_REGISTRATION_PER_TIMEFRAME"] || 40).to_i + max_registrations = ENV.fetch("MAMPF_MAX_REGISTRATION_PER_TIMEFRAME", 40).to_i return if num_new_registrations <= max_registrations # Current number of new registrations is too high diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 50f6b333b..f267191e3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -16,7 +16,7 @@ def current_lecture def host if Rails.env.production? # rubocop:disable Style/StringConcatenation - ENV.fetch("MEDIA_SERVER", nil) + "/" + ENV.fetch("INSTANCE_NAME", nil) + ENV.fetch("MEDIA_SERVER") + "/" + ENV.fetch("INSTANCE_NAME") # rubocop:enable Style/StringConcatenation else "" @@ -29,7 +29,7 @@ def host # the actual media server. # This is used for the download buttons for videos and manuscripts. def download_host - Rails.env.production? ? ENV.fetch("DOWNLOAD_LOCATION", nil) : "" + Rails.env.production? ? ENV.fetch("DOWNLOAD_LOCATION") : "" end # Returns the full title on a per-page basis. diff --git a/app/mailers/exception_handler/exception_mailer.rb b/app/mailers/exception_handler/exception_mailer.rb index 84db08e96..f56a25431 100644 --- a/app/mailers/exception_handler/exception_mailer.rb +++ b/app/mailers/exception_handler/exception_mailer.rb @@ -5,7 +5,7 @@ class ExceptionMailer < ApplicationMailer # Defaults default subject: I18n.t("exception.exception", - host: ENV.fetch("URL_HOST", nil)) + host: ENV.fetch("URL_HOST")) default from: ExceptionHandler.config.email default template_path: "exception_handler/mailers" # => http://stackoverflow.com/a/18579046/1143732 diff --git a/app/models/user_cleaner.rb b/app/models/user_cleaner.rb index 877760420..8a5763e53 100644 --- a/app/models/user_cleaner.rb +++ b/app/models/user_cleaner.rb @@ -3,9 +3,9 @@ class UserCleaner attr_accessor :imap, :email_dict, :hash_dict def login - @imap = Net::IMAP.new(ENV.fetch("IMAPSERVER", nil), port: 993, ssl: true) - @imap.authenticate("LOGIN", ENV.fetch("PROJECT_EMAIL_USERNAME", nil), - ENV.fetch("PROJECT_EMAIL_PASSWORD", nil)) + @imap = Net::IMAP.new(ENV.fetch("IMAPSERVER"), port: 993, ssl: true) + @imap.authenticate("LOGIN", ENV.fetch("PROJECT_EMAIL_USERNAME"), + ENV.fetch("PROJECT_EMAIL_PASSWORD")) end def logout @@ -15,7 +15,7 @@ def logout def search_emails_and_hashes @email_dict = {} @hash_dict = {} - @imap.examine(ENV.fetch("PROJECT_EMAIL_MAILBOX", nil)) + @imap.examine(ENV.fetch("PROJECT_EMAIL_MAILBOX")) # Mails containing multiple email addresses (Subject: "Undelivered Mail Returned to Sender") @imap.search(["SUBJECT", "Undelivered Mail Returned to Sender"]).each do |message_id| @@ -89,11 +89,11 @@ def send_hashes end def delete_ghosts - @hash_dict.each do |mail, hash| - u = User.find_by(email: mail, ghost_hash: hash) - move_mail(@email_dict[mail]) if u.present? && @email_dict.present? - u.destroy! if u&.generic? - end + # @hash_dict.each do |mail, hash| + # u = User.find_by(email: mail, ghost_hash: hash) + # move_mail(@email_dict[mail]) if u.present? && @email_dict.present? + # u.destroy! if u&.generic? + # end end def move_mail(message_ids, attempt = 0) @@ -103,7 +103,7 @@ def move_mail(message_ids, attempt = 0) return if attempt > 3 begin - @imap.examine(ENV.fetch("PROJECT_EMAIL_MAILBOX", nil)) + @imap.examine(ENV.fetch("PROJECT_EMAIL_MAILBOX")) @imap.move(message_ids, "Other Users/mampf/handled_bounces") rescue Net::IMAP::BadResponseError move_mail(message_ids, attempt + 1) @@ -111,14 +111,15 @@ def move_mail(message_ids, attempt = 0) end def clean! - login - search_emails_and_hashes - return if @email_dict.blank? - - send_hashes - sleep(10) - search_emails_and_hashes - delete_ghosts - logout + # TODO: Implement new user cleaner logic + # login + # search_emails_and_hashes + # return if @email_dict.blank? + + # send_hashes + # sleep(10) + # search_emails_and_hashes + # delete_ghosts + # logout end end diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index 2cb17d595..2e47ea0f0 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -33,13 +33,13 @@ target: :_blank)), class: "d-inline", for: "dsgvo-consent" %> <%= f.hidden_field :locale, value: I18n.locale %> - <% if ENV['USE_CAPTCHA_SERVICE']%> + <% if ENV["USE_CAPTCHA_SERVICE"]%>

-

+

<%end %>
- <%= f.submit t('.sign_up'), class: 'btn btn-primary', id: 'register-user', disabled:ENV['USE_CAPTCHA_SERVICE'] %> + <%= f.submit t('.sign_up'), class: 'btn btn-primary', id: 'register-user', disabled:ENV.fetch("USE_CAPTCHA_SERVICE") %>
<% end %> diff --git a/app/views/exception_handler/mailers/new_exception.html.erb b/app/views/exception_handler/mailers/new_exception.html.erb index 93b3b9b89..0ebfbbe16 100644 --- a/app/views/exception_handler/mailers/new_exception.html.erb +++ b/app/views/exception_handler/mailers/new_exception.html.erb @@ -1,5 +1,5 @@ <%= t('exception.exception_report', - host: ENV['URL_HOST']) %> + host: ENV.fetch("URL_HOST")) %> <%= @exception.response %> (<%= @exception.status %>) diff --git a/app/workers/interaction_saver.rb b/app/workers/interaction_saver.rb index f6c95faf1..344d53830 100644 --- a/app/workers/interaction_saver.rb +++ b/app/workers/interaction_saver.rb @@ -2,8 +2,8 @@ class InteractionSaver include Sidekiq::Worker def perform(session_id, full_path, referrer, study_participant) - referrer_url = if referrer.to_s.include?(ENV["URL_HOST"]) - referrer.to_s.remove(ENV["URL_HOST"]) + referrer_url = if referrer.to_s.include?(ENV.fetch("URL_HOST")) + referrer.to_s.remove(ENV.fetch("URL_HOST")) .remove("https://").remove("http://") end Interaction.create(session_id: Digest::SHA2.hexdigest(session_id).first(10), diff --git a/config/application.rb b/config/application.rb index 15040fd01..b5e4e0f1e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -24,7 +24,7 @@ class Application < Rails::Application # the framework and any gems in your application. config.exception_handler = { # sends exception emails to a listed email (string // "you@email.com") - email: ENV.fetch("ERROR_EMAIL", nil), + email: ENV.fetch("ERROR_EMAIL"), # All keys interpolated as strings, so you can use # symbols, strings or integers where necessary diff --git a/config/environments/production.rb b/config/environments/production.rb index 805b5c66e..ae9c17db1 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -55,13 +55,13 @@ config.log_tags = [:request_id] # Use a different cache store in production. - config.cache_store = :mem_cache_store, ENV.fetch("MEMCACHED_SERVER", nil) + config.cache_store = :mem_cache_store, ENV.fetch("MEMCACHED_SERVER") # 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 = "mampf_#{Rails.env}" config.action_mailer.perform_caching = false - config.action_mailer.default_url_options = { protocol: "https", host: ENV.fetch("URL_HOST", nil) } + config.action_mailer.default_url_options = { protocol: "https", host: ENV.fetch("URL_HOST") } config.action_mailer.delivery_method = :smtp config.action_mailer.perform_deliveries = true @@ -69,9 +69,10 @@ config.action_mailer.default(charset: "utf-8") config.action_mailer.smtp_settings = { - address: ENV.fetch("MAILSERVER", nil), + address: ENV.fetch("MAILSERVER"), port: 25, - domain: ENV.fetch("MAILSERVER", nil) + user_name: ENV.fetch("MAMPF_EMAIL_USERNAME"), + password: ENV.fetch("MAMPF_EMAIL_PASSWORD") } # Ignore bad email addresses and do not raise email delivery errors. diff --git a/config/initializers/default_setting.rb b/config/initializers/default_setting.rb index bdc25a5da..49d0ea455 100644 --- a/config/initializers/default_setting.rb +++ b/config/initializers/default_setting.rb @@ -1,10 +1,10 @@ class DefaultSetting - ERDBEERE_LINK = ENV.fetch("ERDBEERE_SERVER", nil) - MUESLI_LINK = ENV.fetch("MUESLI_SERVER", nil) - PROJECT_EMAIL = ENV.fetch("PROJECT_EMAIL", nil) - PROJECT_NOTIFICATION_EMAIL = ENV.fetch("PROJECT_NOTIFICATION_EMAIL", nil) - BLOG_LINK = ENV.fetch("BLOG", nil) - URL_HOST_SHORT = ENV.fetch("URL_HOST_SHORT", nil) + ERDBEERE_LINK = ENV.fetch("ERDBEERE_SERVER") + MUESLI_LINK = ENV.fetch("MUESLI_SERVER") + PROJECT_EMAIL = ENV.fetch("PROJECT_EMAIL") + PROJECT_NOTIFICATION_EMAIL = ENV.fetch("PROJECT_NOTIFICATION_EMAIL") + BLOG_LINK = ENV.fetch("BLOG") + URL_HOST_SHORT = ENV.fetch("URL_HOST_SHORT") RESEARCHGATE_LINK = "https://www.researchgate.net/project/MaMpf-Mathematische-Medienplattform".freeze TOUR_LINK = "https://mampf.blog/ueber-mampf/".freeze RESOURCES_LINK = "https://mampf.blog/ressourcen-fur-editorinnen/".freeze diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index c3b179a48..d900ea888 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -12,7 +12,7 @@ # note that it will be overwritten if you use your own mailer class # with default "from" parameter. config.mailer_sender = if Rails.env.production? - ENV.fetch("FROM_ADDRESS", nil) + ENV.fetch("FROM_ADDRESS") else "please-change-me-at-config-initializers-devise@example.com" end diff --git a/config/initializers/thredded.rb b/config/initializers/thredded.rb index 75f4539e3..30b0ae4a3 100644 --- a/config/initializers/thredded.rb +++ b/config/initializers/thredded.rb @@ -190,4 +190,4 @@ # # add in (must install separate gem (under development) as well): # Thredded.notifiers = [Thredded::EmailNotifier.new, -# Thredded::PushoverNotifier.new(ENV.fetch("PUSHOVER_APP_ID", nil))] +# Thredded::PushoverNotifier.new(ENV.fetch("PUSHOVER_APP_ID"))] diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index d7dd28f20..c1856e773 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -81,5 +81,36 @@ RUN bundle install RUN yarn install --production=true COPY --chown=app:app . /usr/src/app + +# The command ". ./docker-dummy.env" will source our dummy docker env file. +# So why do we need this? +# +# Well, (deeply inhales), Rails needs to boot entirely to run the +# `assets:precompile` task. Therefore, it also needs to access the env variables +# to correctly start the initializers. +# +# However (after a long ime researching), docker compose does not seem to offer +# an easy solution to have an env file from the host machine available during +# the build step (Dockerfile) and not just during the run time of the container. +# Note that the env file is indeed available on our host, just not in the build +# context, the latter being the MaMpf github repo that docker compose pulls from. +# +# Even with volumes and bind mounts it's not working properly ("file not found"). +# In the end, we found a solution that suggests to use the new docker buildkit +# to allow for multiple build contexts. Yet, we explicitly set DOCKER_BUILDKIT=0 +# to use the old buildkit since the new one always gives a moby-related ssh error. +# And even if this worked, it is not entirely clear if this is even working +# with docker compose or just with docker (sigh). +# +# That's why, in the end, we decided to leverage our already-existing dummy env +# file and source it here in the Dockerfile just to have the precompile task run +# successfully (this task doesn't even rely on the actual values, so despite +# being a hack, it should be fine). +# +# I've written down more details in this question on StackOverflow: +# https://stackoverflow.com/q/78098380/ +COPY ./docker/production/docker.env ./docker-dummy.env + RUN cp -r $(bundle info --path sidekiq)/web/assets /usr/src/app/public/sidekiq && \ - SECRET_KEY_BASE="$(bundle exec rails secret)" DB_ADAPTER=nulldb bundle exec rails assets:precompile + set -o allexport && . ./docker-dummy.env && set +o allexport && \ + SECRET_KEY_BASE="$(bundle exec rails secret)" DB_ADAPTER=nulldb bundle exec rails assets:precompile diff --git a/docker/production/docker.env b/docker/production/docker.env index 8070eb0d9..11523738e 100644 --- a/docker/production/docker.env +++ b/docker/production/docker.env @@ -1,4 +1,5 @@ -GIT_BRANCH=production +# Instance variables +GIT_BRANCH=main COMPOSE_PROJECT_NAME=mampf INSTANCE_NAME=mampf @@ -10,19 +11,29 @@ MUESLI_SERVER=https://muesli.mathi.uni-heidelberg.de ERDBEERE_API=https://erdbeere.mathi.uni-heidelberg.de/api/v1 MEMCACHED_SERVER=cache -# Email is send using a mailserver without authentication. Specify how to connect here +# Email FROM_ADDRESS=mampf@mathi.uni-heidelberg.de MAILSERVER=mail.mathi.uni-heidelberg.de PROJECT_EMAIL=mampf@mathi.uni-heidelberg.de -ERROR_EMAIL=mampf-error@mathi.uni-heidelberg.de +PROJECT_NOTIFICATION_EMAIL=notificationmail MAILID_DOMAIN=mathi.uni-heidelberg.de +ERROR_EMAIL=mampf-error@mathi.uni-heidelberg.de IMAPSERVER=mail.mathi.uni-heidelberg.de PROJECT_EMAIL_USERNAME=creativeusername PROJECT_EMAIL_PASSWORD=secretsecret -PROJECT_EMAIL_MAILBOX=Other Users/mampf +PROJECT_EMAIL_MAILBOX="Other Users/mampf" +MAMPF_EMAIL_USERNAME=secret +MAMPF_EMAIL_PASSWORD=secret # Due to CORS constraints, some urls are proxied to the media server DOWNLOAD_LOCATION=https://mampf.mathi.uni-heidelberg.de/mediaforward +REWRITE_ENABLED=1 + +# Captcha Server +USE_CAPTCHA_SERVICE=1 +CAPTCHA_VERIFY_URL=https://captcha2go.mathi.uni-heidelberg.de/v1/verify +CAPTCHA_PUZZLE_URL=https://captcha2go.mathi.uni-heidelberg.de/v1/puzzle +CAPTCHA_APPLICATION_TOKEN=secret # Upload folder MEDIA_PATH=/private/media @@ -35,14 +46,14 @@ PRODUCTION_DATABASE_INTERACTIONS=mampf_interactions PRODUCTION_DATABASE_HOST=db PRODUCTION_DATABASE_USERNAME=mampf PRODUCTION_DATABASE_PASSWORD=supersecret -PRODUCTION_DATABASE_PORT=5432 -PRODUCTION_DATABASE_URL='postgresql://mampf:supersecret@db:5432/mampf' +PRODUCTION_DATABASE_PORT=port +PRODUCTION_DATABASE_URL='postgresql://mampf:supersecret@db:port/mampf' # Rails configuration # change RAILS_ENV to production for a production deployment RAILS_ENV=production -RAILS_MASTER_KEY= -SECRET_KEY_BASE= +RAILS_MASTER_KEY=secret +SECRET_KEY_BASE=secret URL_HOST=mampf.mathi.uni-heidelberg.de URL_HOST_SHORT=http://mampf.media diff --git a/eslint.config.mjs b/eslint.config.mjs index f037398b8..38122f670 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -82,5 +82,9 @@ export default [ ...globals.node, }, }, + linterOptions: { + // see https://github.com/Splines/eslint-plugin-erb/releases/tag/v2.0.1 + reportUnusedDisableDirectives: "off", + }, }, ]; diff --git a/lib/tasks/db.rake b/lib/tasks/db.rake index d7aa4dd01..067cd8ac2 100644 --- a/lib/tasks/db.rake +++ b/lib/tasks/db.rake @@ -33,7 +33,7 @@ namespace :db do desc "Dumps the database to backups" task dump: :environment do - dump_fmt = ensure_format(ENV.fetch("format", nil)) + dump_fmt = ensure_format(ENV.fetch("format")) dump_sfx = suffix_for_format(dump_fmt) backup_dir = backup_directory(Rails.env, create: true) full_path = nil @@ -56,10 +56,10 @@ namespace :db do namespace :dump do desc "Dumps a specific table to backups" task table: :environment do - table_name = ENV.fetch("table", nil) + table_name = ENV.fetch("table") if table_name.present? - dump_fmt = ensure_format(ENV.fetch("format", nil)) + dump_fmt = ensure_format(ENV.fetch("format")) dump_sfx = suffix_for_format(dump_fmt) backup_dir = backup_directory(Rails.env, create: true) full_path = nil @@ -92,7 +92,7 @@ namespace :db do desc "Restores the database from a backup using PATTERN" task restore: :environment do - pattern = ENV.fetch("pattern", nil) + pattern = ENV.fetch("pattern") if pattern.present? file = nil diff --git a/yarn.lock b/yarn.lock index b58fa340d..dd7907d62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3237,9 +3237,9 @@ escape-string-regexp@^4.0.0: integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== eslint-plugin-erb@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-erb/-/eslint-plugin-erb-2.0.0.tgz#34ef70ff7f04987f3f385a5d498ce4992b504bb7" - integrity sha512-yLfDeaVjY5PzoBR9mk1hRIlOZ/LuzIi8CSZi2DYsRWVY0WIQLRL/D6372OFwhyJomqsnT17V6XRhi6rbeumUhw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-erb/-/eslint-plugin-erb-2.0.1.tgz#ed4b98e2c06221510ff46e43743236a11c0d2a71" + integrity sha512-d0vvv0QNH2MGl69ey89hAKtrfllgB7gSVU85q+eb+/u5OmbUuOtE1QttBZbIoGFnBoDIiTYl+bXaPbqWKB/r5w== eslint-scope@^4.0.3: version "4.0.3"