I'm trying to switch to Zeitwerk in an existing, older Gem (Rails::Engine). Until now all files have been manually require
d and autoload
ed. Plus the engine's lib-folder was added to autoload_paths via config.autoload_paths = paths["lib"].to_a
in class MyEngine < Rails::Engine
.
The switch to use Zeitwerk worked fine via the described way on the Readme:
require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
.
. --> more project specific stuff here
.
loader.setup # ready!
So far so good! Now I want to use the Gem in a Rails app and add the engine's directories to the autoload_path of the Rails application. This worked fine previously via the above mentioned config.autoload_paths
. If I do this now, it fails with the following error message:
Zeitwerk::Error:
loader
#<Zeitwerk::Loader:0x00000001094d4bd0
...
wants to manage directory /gems/<NameOfGem>/lib, which is already managed by
#<Zeitwerk::Loader:0x0000000106b2d728
...
What is the correct way to add the engine's lib-directories to the autoload path of the Rails application?
Thank you!
CodePudding user response:
Rails sets up two loaders main
and once
:
Rails.autoloaders.main
Rails.autoloaders.once
These are just instances of Zeitwerk::Loader
. Rails also gives you a config to add root directories to these loaders:
config.autoload_paths # main
config.autoload_once_paths # once
When gem's lib
directory is added to autoload through one of these configs, lib becomes a root directory:
# config.autoload_paths = paths["lib"].to_a
>> Rails.autoloaders.main.root_dirs
=>
...
"/home/alex/code/stackoverflow/my_engine/lib"=>Object,
...
When a class from the gem is called, zeitwerk uses registered loaders to look up and to load the file corresponding to this class.
If the gem then sets up its own loader:
require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.setup
another instance of Zeitwerk::Loader
is created with its own root directories:
>> Zeitwerk::Registry.loaders.detect { |z| z.tag == "my_engine" }
=>
#<Zeitwerk::GemLoader:0x00007fe5e53e0f80
...
@root_dirs={"/home/alex/code/stackoverflow/my_engine/lib"=>Object},
...
# NOTE: these are the two loaders registered by rails
>> Zeitwerk::Registry.loaders.select { |z| z.tag =~ /rails/ }.count
=> 2
Zeitwerk doesn't allow two loaders to have a shared directory and raises an error showing two conflicting loaders.
Because the gem is a Rails::Engine the best option is to let rails manage zeitwerk loaders and remove Zeitwerk::Loader.for_gem setup.
# only use rails config
config.autoload_paths = paths["lib"].to_a
On the other hand, gem loader is already set up and config.autoload_paths is not needed.
# NOTE: without any loaders
>> MyEngine::Test
# (irb):1:in `<main>': uninitialized constant MyEngine::Test (NameError)
# MyEngine::Test
# ^^^^^^
# NOTE: with gem loader
#
# require "zeitwerk"
# loader = Zeitwerk::Loader.for_gem
# loader.setup
#
>> MyEngine::Test
=> MyEngine::Test
# NOTE: with rails `main` loader
#
# config.autoload_paths = paths["lib"].to_a
#
>> MyEngine::Test
=> MyEngine::Test
# NOTE: with gem loader and rails loader
$ bin/rails c
# /home/alex/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.0/lib/zeitwerk/loader.rb:480:in
# `block (3 levels) in raise_if_conflicting_directory':
# loader (Zeitwerk::Error)