Add code auto-loading to a Ruby project with Zeitwerk
Zeitwerk is a Ruby code auto-loader written to replace Rails’ code auto-loading method with the release of Rails 6. While it’s a relatively new tool, and most of the discussion about it is within the context of Rails, it’s really easy to use it in your own non-Rails Ruby projects.
Why use Zeitwerk?
Loading code is a “solved problem”, and there’s no reason to use a code loader like Zeitwerk other than pure conveience. But, convenient it is! Never worry about code loading order - just set up Zeitwerk and use your application constants without fear of them not being defined properly. Zeitwerk is battle-tested and is the default code loader in Rails 6.
Basics
Project structure
A good Ruby project has a good project directory and file structure. The most generally-accepted structure is what the Zeitwerk documentation calls a “conventional file structure”, and it’s the structure that’ll need to be followed (generally) when using Zeitwerk to load code automatically. Here’s the example given by the Zeitwerk documentation:
lib/my_gem.rb -> MyGem
lib/my_gem/foo.rb -> MyGem::Foo
lib/my_gem/bar_baz.rb -> MyGem::BarBaz
lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
In this structure, the directories and files are named based on the modules or classes that they define. Your project doesn’t necessarily have to be a gem.
Zeitwerk also allows adding multiple directories to the root namespace. Rails is a great example of this, that you’re probably familiar with:
app/models/user.rb -> User
app/controllers/admin/users_controller.rb -> Admin::UsersController
In this example, both app/models
and app/controllers
act as directories belonging to the root namespace. This allows you to structure your project files in a more abstract way, while still maintaining code auto-loading functionality.
Setting up
To add auto-loading to your existing code, you’ll need to add some code before the definition of the main module of your gem or application. In a Rack application, this would be either config.ru
, or the main Ruby file which is included within config.ru
to start the application. In a gem, this would be whichever file first defines the outermost module - the one named the same as the gem.
require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.setup
module CoolGem
# your application code
end
That’s it! Now Zeitwerk is doing all the hard work. Any constants that are referenced within your application will now be loaded on-demand, rather than through require
statements.
Remove unnecessary require statements
Now that we don’t need to use require
statements to require code within our own project, we can now remove them. Remember, this does not include require statements for:
- Other third-party gems your application depends on
- Ruby core and standard libraries (think
yaml
,json
, orfileutils
)
Those require statements aren’t the ones that are a pain, though!
Eager loading code
By default, Zeitwerk will load definitions for constants used in your application on-demand. This is fine, and maybe the desired effect, especially if you have a lot of code to load, and startup-time performance is a concern. If it’s not, or you have a reasonably-sized project, I’d suggest enabling eager loading.
Eager loading will evaluate the entire source tree and load all code automatically, rather than doing it on-demand. This also comes with the added benefit of knowing whether or not your application is structured properly, and all code can be loaded, even if it isn’t necessarily reached during runtime - all at startup.
To enable eager loading, call #eager_load
on the Zeitwerk loader after the end of the main module definition.
require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.setup
module CoolGem
# your application code
end
loader.eager_load
Auto reloading code
If you are developing an application which would benefit from code auto-reloading, like a web application, Zeitwerk supports this out of the box. Before calling loader.setup
, call enable_reloading
:
# ...
loader = Zeitwerk::Loader.for_gem
loader.enable_reloading
loader.setup
# ...
At this point, any time you call loader.reload
, Zeitwerk will reload the application’s code. In order to trigger this automatically, you’ll need a gem which can monitor your files for changes. listen
is perfect for this.
After installing listen
, set it up in the same file you do the Zeitwerk setup in, after you’ve called loader.setup
:
require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.enable_reloading
loader.setup
Listen.to("lib", "config") { loader.reload }.start
module CoolGem
# your application code
end
loader.eager_load
The listener runs in a separate thread and is non-blocking. Viola, you have auto-reloading code!
Happy hacking!