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)
Update
# Use rails loaders
# config.autoload_path .-> Zeitwerk::Loader(@tag=rails.main)
# config.autoload_once_path |-> Zeitwerk::Loader(@tag=rails.once)
# |
# Or create a new loader |
# Zeitwerk::Loader.for_gem |-> Zeitwerk::GemLoader(@tag=my_engine)
# |
# my_engine/lib can only be in one of these
Zeitwerk does the loading and reloading. Rails is just another gem here.
If you don't use rails config, Zeitwerk will find files through Zeitwerk::GemLoader(@tag=my_engine)
, that the gem has created.
If you use rails config, Zeitwerk will find files through Zeitwerk::Loader(@tag=rails.main)
, that rails has created (making GemLoader unnecessary).
If lib is a root directory in any of the existing loaders there is no need to have any requires or autoloads for files in lib directory. Except for things that are needed before Zeitwerk kicks in, like MyEngine::Engine from lib/my_engine/engine.rb.