Now, you can define namespaces with manual constant assignments too (think, Namespace = Struct.new).
To the best of my knowledge, this completes the most important goal of the gem: to fully match Ruby. 🎉
Let me elaborate a bit on why that was not supported before, and how we have been able to remove that long-standing limitation.
First, let's take a step back to understand a tricky aspect of namespaces. Consider this Hotel namespace (in Ruby, both classes and modules can be namespaces):
# hotel.rb
class Hotel
include Pricing
end
# hotel/pricing.rb
module Hotel::Pricing
# Pricing related logic extracted to a mixin.
end
You cannot load any of the two files in regular Ruby, right? To load hotel.rb you need Hotel::Pricing in place, but if you try to load hotel/pricing.rb, you need Hotel!
But that works if the project is managed by Zeitwerk, and it worked with the classic autoloader too.
To support that use case, the :class event of TracePoint was instrumental, and it was the only technique I could think of in 2018.
It worked this way: When TracePoint notifies that Hotel has been created, the loader scans its subdirectories and defines autoloads (issues Module#autoload calls) for child constants, in particular for Hotel::Pricing.
The key observation is that happens precisely before the include line is executed. When the interpreter hits that line, the autoload is in place and autoloading hotel/pricing.rb is possible (because Hotel exists!).
Problem was, the event was not triggered for manual constant assignments (think Hotel = Class.new). So, while that was a corner case, unfortunately we could not achieve full parity with this technique.
The most important and common way to the define namespaces, using the class/module keywords, worked. But argghhhh.
But the suffering is over my friends!
In part motivated by this problem, @_byroot implemented Module#const_added. That callback is invoked in both situations! 🤘
Thanks to Module#const_added, the limitation has been finally removed. Also, Zeitwerk no longer uses TracePoint internally (/cc @yukihiro_matz).
Zeitwerk 2.7 requires Ruby 3.2 or later, and upgrading should be seamless for existing projects.
Don't worry if your app uses an older Ruby. Bundler realizes 2.7's Ruby version constraint is not satisfied, and ignores it in that case. Gemfile.lock will always get a version compatible with your app.
Same for gems that need to support Ruby versions older than 3.2, a version constraint for zeitwerk like ~> 2.6 will do the job. Client code will automatically use a compatible one.
Mission complete! 🎉
• • •
Missing some Tweet in this thread? You can try to
force a refresh
People migrating Rails apps from classic to zeitwerk told me they needed an estimation.
The task zeitwerk:check eager loads. Name mismatches trigger built-in error logic, and you cannot continue because doing so would be unpredictable.
But you can get some sense of "progress":
1) In classic mode, eager load the application by setting config.eager_load = true in config/environments/development.rb.
Then, iterate over ActiveSupport::Dependencies.autoloaded_constants, and print their const_source_location.
2) After that, boot in zeitwerk mode with eager loading disabled and throw config.after_initialize { pp Rails.autoloaders.main.all_expected_cpaths } in config/application.rb.