devise icon indicating copy to clipboard operation
devise copied to clipboard

Rails6 without ActionMailer won't boot with zeitwerk eager-loading

Open sbull opened this issue 6 years ago • 11 comments

Environment

  • Ruby 2.6.3
  • Rails 6.0.0
  • Devise 4.7.1

Current behavior

In a Rails 6 app without ActionMailer included (rails new testapp --skip-action-mailer), the default zeitwerk loader errors when eager-loading due to mailers/devise/mailer.rb not defining Devise::Mailer. Output from rails zeitwerk:check: expected file /usr/local/bundle/gems/devise-4.7.1/app/mailers/devise/mailer.rb to define constant Devise::Mailer,

Expected behavior

Zeitwerk eager-loading should work without ActionMailer. Suggested options:

  • (easy) Define a dummy Devise::Mailer when ActionMailer is not defined.
  • (hard?) Customize the zeitwerk loader for when ActionMailer is not used.

sbull avatar Sep 24 '19 03:09 sbull

Idiomatically, you want to ignore that file. It could done with something like this in Devise (off the top of my head):

initializer "devise.configure_zeitwerk_if_enabled" do
  if Rails.autoloaders.zeitwerk_enabled? && !defined?(ActionMailer)
    Rails.autoloaders.main.ignore("#{__dir__}/relative/path/to/devise/mailer.rb")
  end
end

fxn avatar Sep 24 '19 08:09 fxn

@fxn I would only add a check for Rails::Version::MAJOR >= "6" so it does not break on older versions of Rails where the method zeitwerk_enabled? does not exist, wdyt?

mracos avatar Sep 26 '19 21:09 mracos

Oh yes, totally.

fxn avatar Sep 26 '19 21:09 fxn

In case others come here in search for a quick solution:

It's possibly to work around the issue by simply adding ActionMailer in config/application.rb along with the other frameworks:

require "action_mailer/railtie"

jeppester avatar Oct 11 '19 08:10 jeppester

For what it's worth, in a Rails 6.0.1 app without the ActionMailer railtie, it seems that defining Devise::Mailer as an empty class does not break anything. The app sends emails via a 3rd party service.

# <path-to-gems>/devise-4.7.1/app/mailers/devise/mailer.rb

if defined?(ActionMailer)
  class DeviseMailer < Devise.parent_mailer.constantize
    # whatever was already there
  end
else
  class DeviseMailer
  end
end

Tested authentication, registration, validation, forgot password flows.

Obviously inferior solution to explicitly ignoring the file in an initializer, but thought I'd share.

gkats avatar Nov 13 '19 17:11 gkats

Hi everyone, I'm working on this and I was wondering how can I write a test to check whether the file has been ignored or not please as I don't have an in-depth Rails API knowledge ?

This is what I added so far (else statement) :

if defined?(ActionMailer)
  class Devise::Mailer < Devise.parent_mailer.constantize
    # whatever was already there
else
  if Rails::Version::MAJOR >= "6"
    Rails.autoloaders.main.ignore("#{__dir__}/relative/path/to/devise/mailer.rb")
  end
end

Thank you for your help.

NeimadTL avatar Mar 04 '20 21:03 NeimadTL

Out of curiosity, why isn't the preferred solution here simply for Devise to start using "zeitwerk-correct" file structure and class naming?

rusterholz avatar Mar 12 '21 20:03 rusterholz

Fun story, I just hit this while building a small test app to validate a few other things I'm working on here. I'll get a patch ready for Devise. Thanks everyone!

@rusterholz would you mind clarifying what "using "zeitwerk-correct" file structure and class naming?" means? Devise is using the correct naming structure for that mailer for example, the issue is that it doesn't declare the constant name if ActionMailer isn't loaded (otherwise it'd cause a possible load error), resulting in a devise/mailer.rb file being declared empty, and Zeitwerk doesn't like it, it expects the file to have a matching constant name.

The fix is to either declare that constant name "empty", or to have Zeitwerk ignore that file like mentioned above. (alternatively, loading ActionMailer works too because it makes Devise define the "missing" constant.)

carlosantoniodasilva avatar Aug 30 '21 13:08 carlosantoniodasilva

The main autoloader is setup late in the boot process. An initializer could do something like this (untested)

if Rails.autoloaders.zeitwerk_enabled?
  Rails.autoloaders.main.ignore("#{root}/app/mailers")
end

While classic is gone in Rails 7, zeitwerk_enabled? still exists for engines.

fxn avatar Aug 30 '21 14:08 fxn

Would devise even work without Action Mailer? So many of its default modules requires a mailer to exist.

rafaelfranca avatar Jun 09 '23 23:06 rafaelfranca

@rafaelfranca If you provide a class that provides the necessary interface and responds with objects that respond to the deliver_* methods, it should work properly. For instance I do so with a class that will then call the Sendgrid API to use templates hosted on it.

I just faced the present issue when removing the call to require "action_mailer/railtie" in application.rb.

simonc avatar Feb 18 '24 23:02 simonc