The second beta of Rails 6 is about to ship, and includes Zeitwerk integration.
While Rails 6 final is going to have proper documentation about it, this post will help understanding this feature meanwhile.
You can now robustly use constant paths in class and module definitions:
# Autoloading in this class' body matches Ruby semantics now.
class Admin::UsersController < ApplicationController
# ...
end
All known use cases of require_dependency
have been eliminated.
Edge cases in which the autoloaded constant depended on execution order are gone.
Autoloading is thread-safe in general, not just for the currently supported use cases with explicit locks like web requests. For example, you can now write multi-threaded scripts to be run by bin/rails runner
and they will autoload just fine.
Also, applications get some performance benefits for the same price:
Autoloading a constant no longer involves walking autoload paths looking for a relative match in the file system. Zeitwerk does one single pass and autoloads everything using absolute file names. In addition, that single pass descends into subdirectories lazily, only when the namespaces are used.
When an application is reloaded due to changes in the file system, code in the autoload paths of engines that were loaded as gems won’t be reloaded anymore.
Eager loading eager loads not only the application, but also the code of any gem that is managed by Zeitwerk.
Rails 6 ships with two autoloading modes: :zeitwerk
and :classic
. They are set using the new configuration point config.autoloader
.
Zeitwerk mode is the default in Rails 6 for CRuby, automatically enabled by
load_defaults "6.0"
in config/application.rb
.
Applications can opt-out putting
config.autoloader = :classic
after the line that loads the defaults.
While a first API for Zeitwerk mode is converging, this is still a bit exploratory right now. Please, check the current documentation if your version of Rails is newer than 6.0.0.beta2
.
The configuration point for autoload paths remains config.autoload_paths
, and if you push by hand to ActiveSupport::Dependencies.autoload_paths
during application initialization, that will also work.
All known use cases of require_dependency
have been eliminated. In principle, you should just delete all these calls in the code base. See also the next section about STIs.
Active Record needs to have STI hierarchies fully loaded in order to generate correct SQL. Preloading in Zeitwerk was designed for this use case:
# config/initializers/preload_vehicle_sti.rb
autoloader = Rails.autoloaders.main
sti_leaves = %w(car motorbike truck)
sti_leaves.each do |leaf|
autoloader.preload("#{Rails.root}/app/models/#{leaf}.rb")
end
By preloading the leaves of the tree, autoloading will take care of the entire hierarchy upwards following superclasses.
These files are going to be preloaded on boot, and on each reload.
In Zeitwerk mode, Rails.autoloaders
is an enumerable that has two Zeitwerk instances called main
, and once
. The former is the one managing your application, and the latter manages engines loaded as gems, as well as anything in the somewhat unknown config.autoload_once_paths
(whose future is not bright). Rails reloads with main
, and once
is there just for autoloading and eager loading, but does not reload.
Those instances are accessible respectively as
Rails.autoloaders.main
Rails.autoloaders.once
but since Rails.autoloaders
is an enumerable, there won’t be too many use cases for direct access, probably.
If you want to see the autoloaders working, you can throw
Rails.autoloaders.logger = method(:puts)
to config/application.rb
after the framework defaults have been set. In addition to a callable, Rails.autoloaders.logger=
accepts also anything that responds to debug
with arity 1, like regular loggers do.
If you want to see the activity of Zeitwerk for all instances in memory (both Rails’ and others that might be managing gems), you can set
Zeitwerk::Loader.default_logger = method(:puts)
at the top of config/application.rb
, before Bundle.require
.
For files below the standard concerns
directories (like app/models/concerns
), Concerns
cannot be a namespace. That is, app/models/concerns/geolocatable.rb
is expected to define Geolocatable
, not Concerns::Geolocatable
.
Once the application has booted, autoload paths are frozen.
Directories in ActiveSupport::Dependencies.autoload_paths
that do not exist on boot are ignored. We are referring here to the actual elements of the array only, not to their subdirectories. New subdirectories of autoload paths that existed at boot are picked up just fine as always. (This may change for final.)
A file that defines a class or module that acts as a namespace, needs to define the class or module using the class
and module
keywords. For example, if you have app/models/hotel.rb
defining the Hotel
class, and app/models/hotel/pricing.rb
defining a mixin for hotels, the Hotel
class must be defined with class
, you cannot do Hotel = Class.new { ... }
or Hotel = Struct.new { ... }
or anything like that. Those idioms are fine in classes and modules that do not act as namespaces.
A file should define only one constant in its namespace (but can define inner ones). So, if app/models/foo.rb
defines Foo
and also Bar
, Bar
won’t be reloaded, but reopened when Foo
is reloaded. This is strongly discouraged in classic mode anyway though, the convention is to have one single main constant matching the file name. You can have inner constants, so Foo
may define an auxiliary internal class Foo::Woo
.