Rails 7 Engine Initializer Hook Not Working - ruby-on-rails

trying to get my gem working in Rails 7. I have confirmed it works in Rails 6.1.4.1 (latest).
In my engine's engine.rb file I have this...
module MyEngine
class Engine < ::Rails::Engine
isolate_namespace MyEngine
initializer "my_engine.include_controller" do |app|
ActionController::Base.send :include, MyEngine::MyController
end
end
end
Upon server start or running a console I get...
uninitialized constant MyEngine::MyController (NameError)
I have the gem controllers in their namespaced directory and to reiterate this works in Rails 6.1.
I have also tried these variations with the same error...
ActionController::Base.include MyEngine::MyController
ActiveSupport.on_load :action_controller_base do
include MyEngine::MyController
end
If I put the following in the main app's ApplicationController instead then it works...
include MyEngine::MyController
Does anyone have any insight to how these hooks need to be called or should I report this as a bug to the rails team?

I was able to fix the same issue by pushing the required files to the autoload_once_paths configuration. Here an example of a possible fix.
module MyEngine
class Engine < ::Rails::Engine
isolate_namespace MyEngine
config.autoload_once_paths << "#{root}/app/controllers"
initializer "my_engine.include_controller" do |app|
ActionController::Base.send :include, MyEngine::MyController
end
end
end
Sources:
https://edgeguides.rubyonrails.org/autoloading_and_reloading_constants.html#config-autoload-once-paths
https://github.com/charlotte-ruby/impressionist/issues/305
https://github.com/hotwired/turbo-rails/blob/main/lib/turbo/engine.rb

Related

Updating a gem having issues with zeitwerk

I'm trying to update this gem to work with rails 7 and I think that the error is due to zeitwerk. The error that's popping up is the following
/Users/nate/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/impressionist-2.0.0/lib/impressionist/engine.rb:15:in `block (2 levels) in <class:Engine>': uninitialized constant Impressionist::Engine::ImpressionistController (NameError)
and It's originating from lib/impressionist/engine.rb here from the lines below
module Impressionist
class Engine < ::Rails::Engine
attr_accessor :orm
initializer 'impressionist.model' do |app|
#orm = Impressionist.orm
include_orm
end
initializer 'impressionist.controller' do
require "impressionist/controllers/mongoid/impressionist_controller" if orm == :mongoid.to_s
ActiveSupport.on_load(:action_controller) do
include ImpressionistController::InstanceMethods <--- HERE
extend ImpressionistController::ClassMethods <--- HERE
end
end
private
def include_orm
require "#{root}/app/models/impressionist/impressionable.rb"
require "impressionist/models/#{orm}/impression.rb"
require "impressionist/models/#{orm}/impressionist/impressionable.rb"
end
end
end
I think that this first include is trying to call app/controllers/impressionist_controller.rb, but for some reason doesn't load with rails 7. Any ideas on how to approach fixing this? I suspect that I have to somehow incorporate zeitwerk here.

Rails 7 controller decorator uninitialised constant error in production only

I am getting the following error zeitwerk/loader/helpers.rb:95:in const_get': uninitialized constant Controllers::BasePublicDecorator (NameError)
This is an error in a local production console using rails c -e production but not an issue in development which works perfectly.
In an engine, CcsCms::PublicTheme, I have a decorator I am using to extend the controller of another CcsCms::Core engine and it is this decorator that is causing the error.
public_theme/app/decorators/decorators/controllers/base_public_decorator.rb
CcsCms::BasePublicController.class_eval do
before_action :set_theme #ensure that #current_theme is available for the
#header in all public views
private
def set_theme
#current_theme = CcsCms::PublicTheme::Theme.current_theme
end
end
This functionality is working perfectly in development but fails in production with an error as follows
The controller I am trying to decorate in the CcsCms::Core engine is CcsCms::BasePublicController.rb
module CcsCms
class BasePublicController < ApplicationController
layout "ccs_cms/layouts/public"
protected
def authorize
end
end
end
in the theme engine with the decorator I am trying to use I have a Gemfile that defines the core engine as follows
gem 'ccs_cms_core', path: '../core'
In the ccs_cms_public_theme.gemspec I am requiring the core engine as a dependency
spec.add_dependency "ccs_cms_core"
in the engine.rb I am requiring the core engine and loading the decorator paths in a config.to_prepare do block
require "deface"
require 'ccs_cms_admin_dashboard'
require 'ccs_cms_custom_page'
require 'ccs_cms_core'
require 'css_menu'
#require 'tinymce-rails'
require 'delayed_job_active_record'
require 'daemons'
require 'sprockets/railtie'
require 'sassc-rails'
module CcsCms
module PublicTheme
class Engine < ::Rails::Engine
isolate_namespace CcsCms::PublicTheme
paths["app/views"] << "app/views/ccs_cms/public_theme"
initializer "ccs_cms.assets.precompile" do |app|
app.config.assets.precompile += %w( public_theme_manifest.js )
end
initializer :assets do |config|
Rails.application.config.assets.paths << root.join("")
end
initializer :append_migrations do |app|
unless app.root.to_s.match?(root.to_s)
config.paths['db/migrate'].expanded.each do |p|
app.config.paths['db/migrate'] << p
end
end
end
initializer :active_job_setup do |app|
app.config.active_job.queue_adapter = :delayed_job
end
config.to_prepare do
Dir.glob(Engine.root.join("app", "decorators", "**", "*_decorator*.rb")) do |c|
Rails.configuration.cache_classes ? require(c) : load(c)
end
end
config.generators do |g|
g.test_framework :rspec,
fixtures: false,
request: false,
view_specs: false,
helper_specs: false,
controller_specs: false,
routing_specs: false
g.fixture_replacement :factory_bot
g.factory_bot dir: 'spec/factories'
end
end
end
end
Given that my decorator is given the same name as the controller it is decorating from the core engine but with the .decorator extension I am pretty certain that is everything hooked up correctly, as mentioned, this works perfectly in development but I am unable to start a rails console in a production environment due to this error.
It seems that the class_eval is failing somehow and I can only think that this may be a path issue but I can not figure it out
UPDATE
After quite a big learning curve, thank's muchly to #debugger comments and #Xavier Noria
answer it is clear that my issue comes down to Zeitworks autoload functionality
Rails guides here has an interesting and appealing solution to me
Another use case are engines decorating framework classes:
initializer "decorate ActionController::Base" do
> ActiveSupport.on_load(:action_controller_base) do
> include MyDecoration end end
There, the module object stored in MyDecoration by the time the
initializer runs becomes an ancestor of ActionController::Base, and
reloading MyDecoration is pointless, it won't affect that ancestor
chain.
But maybe this isn't the right solution, I again failed to make it work with the following
initializer "decorate CcsCms::BasePublicController" do
ActiveSupport.on_load(:ccs_cms_base_public_controller) do
include CcsCms::BasePublicDecorator
end
end
Generating the following error
zeitwerk/loader/callbacks.rb:25:in `on_file_autoloaded': expected file /home/jamie/Development/rails/comtech/r7/ccs_cms/engines/public_theme/app/decorators/controllers/ccs_cms/base_public_decorator.rb to define constant Controllers::CcsCms::BasePublicDecorator, but didn't (Zeitwerk::NameError)
So back to the solution provided here, thank's again for the answer below I tried the following which did work finally
config.to_prepare do
overrides = Engine.root.join("app", "decorators")
Rails.autoloaders.main.ignore(overrides)
p = Engine.root.join("app", "decorators")
loader = Zeitwerk::Loader.for_gem
loader.ignore(p)
Dir.glob(Engine.root.join("app", "decorators", "**", "*_decorator*.rb")) do |c|
Rails.configuration.cache_classes ? require(c) : load(c)
end
end
Problem here is that when lazy loading, nobody is referencing a constant called ...::BasePublicDecorator. However, Zeitwerk expects that constant to be defined in that file, and the mismatch is found when eager loading.
The solution is to configure the autoloader to ignore the decorators, because you are handling their loading, and because they do not define constants after their names. This documentation has an example. It needs to be adapted to your engine, but you'll see the idea.
For completeness, let me also explain that in Zeitwerk, eager loading is a recursive const_get, not a recursive require. This is to guarantee that if you access the constant, loading succeeds or fails consistently in both modes (and it is also a tad more efficient). Recursive const_get still issues require calls via Module#autoload, and if you ran one for some file idempotence also applies, but Zeitwerk detects the expected constant is not defined anyway, which is an error condition.

Rails 5 Require a dependent gem and its initializers conditionally in a gem railtie

We are writing a gem that includes multiple common gems for a couple of our shared apps. We want to be able to have a config in application.rb or enviroment.rb/*rb something like config.fruit_chain.enable_transport = true from the consuming app to conditionally require a gem and it's initializer dynamically. But the initializer from common gem does not run after require in a railtie. I wondered if there is a better way to do this
fruit_store/config/application.rb . (consuming app)
module FruitStore
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 5.2
config.fruit_chain.enable_transport = true
end
end
fruit_chain/lib/fruit_chain.rb (Our gem)
require analytic
- require transport <----- removed this so it dose not autoload
require marketing
...
module FruitChain
end
fruit_chain/lib/fruit_chain/rails/railtie.rb
module FruitChain
module Rails
class Railtie < ::Rails::Railtie
config.fruit_chain = ActiveSupport::OrderedOptions.new
config.fruit_chain.enable_transport = false
config.before_initialize do |app|
if app.config.fruit_chain.enable_transport
Kernel.require 'transport' <--- this require the gem correct and load it up
app.initializers.find{
|a| a.name === 'transport.configure'
}.run <--- transport.configure initializer doesn't kick off
end
end
end
end
end
transport/lib/transport.rb . (Dependent common gem)
require transport/rails/railtie
...
module Transport
end
transport/lib/transport/rails/railtie.rb
module Transport
module Rails
class Railtie < ::Rails::Railtie
initializer 'transport.configure' do |app|
...
end
end
end
end

Cannot load Sidekiq with nested module in lib

For a Rails project I'm working on, I'm having an issue with loading Sidekiq and having nested modules in the lib directory.
my lib/scraper/v2.rb looks like this:
require 'scraper/v2/client'
module Scraper
module V2
end
end
my lib/scraper/v2/client.rb looks like this:
module Scraper
module V2
class Client
def initialize
...
end
end
end
end
I then have a Sidekiq job in the jobs directory that looks like this:
class RefreshTokenJob < ApplicationJob
queue_as :default
def perform
client = Scraper::V2::Client.new
...
end
end
If I run bundle exec sidekiq with this configuration, Sidekiq starts, but running Scraper::V2::Client.new form the Rails console returns:
NameError: uninitialized constant Scraper::V2
If I add config.autoload_paths += %W(#{config.root}/lib) to my application.rb file, I can run Scraper::V2::Client.new, but starting Sidekiq gives me and uninitialized constant error from a completely different file (within app/jobs/concerns/).
Any help with this would be much appreciated!
You must follow Rails' conventions for naming files if you want Rails autoloading to work correctly.
For a module named Scraper::V2, it should be in a file named scraper/v2.rb, not scraper_v2.rb.

How to reload helpers on rails mountable engine

In my rails mountable engine:
config.to_prepare do
# works fine, and reload automatically in development
ApplicationController.helper :application
# works fine, but doesn't reload. After restart server, it works.
ApplicationController.helper Rails.application.helpers
It looks like fine when arg is symbol or string. But it doesn't work when arg is a module like Rails.application.helpers.
Or is there a good way to get all helpers like [:application, :users] from Rails.application.helpers.
Rails: 4.2.3
You can configure autoload_paths of the engine.
lib/my_engine/engine.rb
module MyEngine
class Engine < ::Rails::Engine
...
config.autoload_paths += Dir[Engine.root.join('app', 'helpers')]
end
end
http://api.rubyonrails.org/classes/Rails/Engine.html
for people using rails 6, you might want to try using the classic autoloader instead of zeitwerk
config.autoloader = :classic

Resources