Skip to content

Commit 55fc642

Browse files
committed
New guide to help migrate from classic to zeitwerk mode
1 parent caf2600 commit 55fc642

File tree

2 files changed

+395
-0
lines changed

2 files changed

+395
-0
lines changed
Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
1+
**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
2+
3+
Classic to Zeitwerk HOWTO
4+
=========================
5+
6+
This guide documents how to migrate Rails applications from `classic` to `zeitwerk` mode.
7+
8+
After reading this guide, you will know:
9+
10+
* What are `classic` and `zeitwerk` modes
11+
* Why switch from `classic` to `zeitwerk`
12+
* How to activate `zeitwerk` mode
13+
* How to verify your application runs in `zeitwerk` mode
14+
* How to verify your project loads OK in the command line
15+
* How to verify your project loads OK in the test suite
16+
* How to address possible edge cases
17+
18+
--------------------------------------------------------------------------------
19+
20+
What are `classic` and `zeitwerk` Modes?
21+
--------------------------------------------------------
22+
23+
From the very beginning, and up to Rails 5, Rails used an autoloader implemented in Active Support. This autoloader is known as `classic` and is still available in Rails 6.x. Rails 7 does not include this autoloader anymore.
24+
25+
Starting with Rails 6, Rails ships with a new and better way to autoload, which delegates to the [Zeitwerk](https://github.com/fxn/zeitwerk) gem. This is `zeitwerk` mode. By default, applications loading the 6.0 and 6.1 framework defaults run in `zeitwerk` mode, and this is the only mode available in Rails 7.
26+
27+
28+
Why Switch from `classic` to `zeitwerk`?
29+
----------------------------------------
30+
31+
The `classic` autoloader has been extremely useful, but had a number of [issues](https://guides.rubyonrails.org/v6.1/autoloading_and_reloading_constants_classic_mode.html#common-gotchas) that made autoloading a bit tricky and confusing at times. Zeitwerk was developed to address them, among other [motivations](https://github.com/fxn/zeitwerk#motivation).
32+
33+
When upgrading to Rails 6.x, it is highly encouraged to switch to `zeitwerk` mode because `classic` mode is deprecated.
34+
35+
Rails 7 ends the transition period and does not include `classic` mode.
36+
37+
I am scared
38+
-----------
39+
40+
Don't :).
41+
42+
Zeitwerk was designed to be as compatible with the classic autoloader as possible. If you have a working application autoloading correctly today, chances are the switch will be easy. Many projects, big and small, have reported really smooth switches.
43+
44+
This guide will help you change the autoloader with confidence.
45+
46+
If for whatever reason you find a situation you don't know how to resolve, don't hesitate to [open an issue in `rails/rails`](https://github.com/rails/rails/issues/new) and tag [`@fxn`](https://github.com/fxn).
47+
48+
49+
How to Activate `zeitwerk` Mode
50+
-------------------------------
51+
52+
### Applications running Rails 5.x or Less
53+
54+
In applications running a Rails version previous to 6.0, `zeitwerk` mode is not available. You need to be in at least Rails 6.0.
55+
56+
### Applications running Rails 6.x
57+
58+
In applications running Rails 6.x there are two scenarios.
59+
60+
If the application is loading the framework defaults of Rails 6.0 or 6.1 and it is running in `classic` mode, it must be opting out by hand. You have to have something similar to this:
61+
62+
```ruby
63+
# config/application.rb
64+
config.load_defaults 6.0
65+
config.autoloader = :classic # DELETE THIS LINE
66+
```
67+
68+
As noted, just delete the override, `zeitwerk` mode is the default.
69+
70+
On the other hand, if the application is loading old framework defaults you need to enable `zeitwerk` mode explictly:
71+
72+
```ruby
73+
# config/application.rb
74+
config.load_defaults 5.2
75+
config.autoloader = :zeitwerk
76+
```
77+
78+
### Applications Running Rails 7
79+
80+
In Rails 7 there is only `zeitwerk` mode, you do not need to do anything to enable it.
81+
82+
Indeed, the setter `config.autoloader=` does not even exist. If `config/application.rb` has it, please just delete the line.
83+
84+
85+
How to Verify The Application Runs in `zeitwerk` Mode?
86+
------------------------------------------------------
87+
88+
To verify the application is running in `zeitwerk` mode, execute
89+
90+
```
91+
bin/rails runner 'p Rails.autoloaders.zeitwerk_enabled?'
92+
```
93+
94+
If that prints `true`, `zeitwerk` mode is enabled.
95+
96+
97+
Does my Application Comply with Zeitwerk Conventions?
98+
-----------------------------------------------------
99+
100+
Once `zeitwerk` mode is enabled, please run:
101+
102+
```
103+
bin/rails zeitwerk:check
104+
```
105+
106+
A successful check looks like this:
107+
108+
```
109+
% bin/rails zeitwerk:check
110+
Hold on, I am eager loading the application.
111+
All is good!
112+
```
113+
114+
There can be additional input depending on the application configuration, but the last "All is good!" is what you are looking for.
115+
116+
If there's any file that does not define the expected constant, the task will tell you. It does so one file at a time, because if it moved on, the failure loading one file could cascade into other failures unrelated to the check we want to run and the error report would be unreliable.
117+
118+
If there's one constant reported, fix that particular one and run the task again. Repeat until you get "All is good!".
119+
120+
Take for example:
121+
122+
```
123+
% bin/rails zeitwerk:check
124+
Hold on, I am eager loading the application.
125+
expected file app/models/vat.rb to define constant Vat
126+
```
127+
128+
VAT is an European tax. The file `app/models/vat.rb` defines `VAT` but the autoloader expects `Vat`, why?
129+
130+
### Acronyms
131+
132+
This is the most common kind of discrepancy you may find, it has to do with acronyms. Let's understand why do we get that error message.
133+
134+
The classic autoloader is able to autoload `VAT` because its input is the name of the missing constant, `VAT`, invokes `underscore` on it, which yields `vat`, and looks for a file called `var.rb`. It works.
135+
136+
The input of the new autoloader is the file system. Give the file `vat.rb`, Zeitwerk invokes `camelize` on `vat`, which yields `Vat`, and expects the file to define the constant `Vat`. That is what the error message says.
137+
138+
Fixing this is easy, you only need to tell the inflector about this acronym:
139+
140+
```ruby
141+
# config/initializers/inflections.rb
142+
ActiveSupport::Inflector.inflections(:en) do |inflect|
143+
inflect.acronym "VAT"
144+
end
145+
```
146+
147+
Doing so affects how Active Support inflects globally. That may be fine, but if you prefer you can also pass overrides to the inflector used by the autoloader:
148+
149+
```ruby
150+
# config/initializers/zeitwerk.rb
151+
Rails.autoloaders.each do |autoloader|
152+
autoloader.inflector.inflect("vat" => "VAT")
153+
end
154+
```
155+
156+
With that in place, the check passes 🎉:
157+
158+
```
159+
% bin/rails zeitwerk:check
160+
Hold on, I am eager loading the application.
161+
All is good!
162+
```
163+
164+
#### Concerns
165+
166+
You can autoload and eager load from a standard structure like
167+
168+
```
169+
app/models
170+
app/models/concerns
171+
```
172+
173+
In that case, `app/models/concerns` is assumed to be a root directory (because it belongs to the autoload paths), and it is ignored as namespace. So, `app/models/concerns/foo.rb` should define `Foo`, not `Concerns::Foo`.
174+
175+
The `Concerns::` namespace worked with the classic autoloader as a side-effect of the implementation, but it was not really an intended behavior. An application using `Concerns::` needs to rename those classes and modules to be able to run in `zeitwerk` mode.
176+
177+
#### Having `app` in the autoload paths
178+
179+
Some projects want something like `app/api/base.rb` to define `API::Base`, and add `app` to the autoload paths to accomplish that in `classic` mode.
180+
181+
Since Rails adds all subdirectories of `app` to the autoload paths automatically, we have another situation in which there are nested root directories, so that setup no longer works. Similar principle we explained above with `concerns`.
182+
183+
If you want to keep that structure, you'll need to delete the subdirectory from the autoload paths in an initializer:
184+
185+
```ruby
186+
# config/initializers/zeitwerk.rb
187+
ActiveSupport::Dependencies.autoload_paths.delete("#{Rails.root}/app/api")
188+
```
189+
190+
#### Autoloaded Constants and Explicit Namespaces
191+
192+
If a namespace is defined in a file, as `Hotel` is here:
193+
194+
```
195+
app/models/hotel.rb # Defines Hotel.
196+
app/models/hotel/pricing.rb # Defines Hotel::Pricing.
197+
```
198+
199+
the `Hotel` constant has to be set using the `class` or `module` keywords. For example:
200+
201+
```ruby
202+
class Hotel
203+
end
204+
```
205+
206+
is good.
207+
208+
Alternatives like
209+
210+
```ruby
211+
Hotel = Class.new
212+
```
213+
214+
or
215+
216+
```ruby
217+
Hotel = Struct.new
218+
```
219+
220+
won't work, child objects like `Hotel::Pricing` won't be found.
221+
222+
This restriction only applies to explicit namespaces. Classes and modules not defining a namespace can be defined using those idioms.
223+
224+
#### One file, one constant (at the same top-level)
225+
226+
In `classic` mode you could technically define several constants at the same top-level and have them all reloaded. For example, given
227+
228+
```ruby
229+
# app/models/foo.rb
230+
231+
class Foo
232+
end
233+
234+
class Bar
235+
end
236+
```
237+
238+
while `Bar` could not be autoloaded, autoloading `Foo` would mark `Bar` as autoloaded too.
239+
240+
This is not the case in `zeitwerk` mode, you need to move `Bar` to its own file `bar.rb`. One file, one top-level constant.
241+
242+
This affects only to constants at the same top-level as in the example above. Inner classes and modules are fine. For example, consider
243+
244+
```ruby
245+
# app/models/foo.rb
246+
247+
class Foo
248+
class InnerClass
249+
end
250+
end
251+
```
252+
253+
If the application reloads `Foo`, it will reload `Foo::InnerClass` too.
254+
255+
#### Spring and the `test` Environment
256+
257+
Spring reloads the application code if something changes. In the `test` environment you need to enable reloading for that to work:
258+
259+
```ruby
260+
# config/environments/test.rb
261+
config.cache_classes = false
262+
```
263+
264+
Otherwise you'll get this error:
265+
266+
```
267+
reloading is disabled because config.cache_classes is true
268+
```
269+
270+
This has no performance penalty.
271+
272+
#### Bootsnap
273+
274+
Please make sure to depend on at least Bootsnap 1.4.4.
275+
276+
277+
Check Zeitwerk Compliance in the Test Suite
278+
-------------------------------------------
279+
280+
The Rake task `zeitwerk:check` just eager loads, because doing so triggers built-in validations in Zeitwerk.
281+
282+
You can add the equivalent of this to your test suite to make sure the application always loads correctly regardless of test coverage:
283+
284+
### minitest
285+
286+
```ruby
287+
require "test_helper"
288+
289+
class ZeitwerkComplianceTest < ActiveSupport::TestCase
290+
test "eager loads all files without errors" do
291+
Zeitwerk::Loader.eager_load_all
292+
rescue => e
293+
flunk(e.message)
294+
else
295+
pass
296+
end
297+
end
298+
```
299+
300+
### RSpec
301+
302+
```ruby
303+
require "rails_helper"
304+
305+
RSpec.describe "Zeitwerk compliance" do
306+
it "eager loads all files without errors" do
307+
expect{ Zeitwerk::Loader.eager_load_all }.not_to raise_error
308+
end
309+
end
310+
```
311+
312+
313+
Delete `require_dependency` calls
314+
---------------------------------
315+
316+
All known use cases of `require_dependency` have been eliminated with Zeitwerk. You should grep the project and delete them.
317+
318+
If your application uses Single Table Inheritance, please see the [Single Table Inheritance section](autoloading_and_reloading_constants.html#single-table-inheritance) of the Autoloading and Reloading Constants (Zeitwerk Mode) guide.
319+
320+
321+
Qualified Names in Class and Module Definitions Are Now Possible
322+
----------------------------------------------------------------
323+
324+
You can now robustly use constant paths in class and module definitions:
325+
326+
```ruby
327+
# Autoloading in this class' body matches Ruby semantics now.
328+
class Admin::UsersController < ApplicationController
329+
# ...
330+
end
331+
```
332+
333+
A gotcha to be aware of is that, depending on the order of execution, the classic autoloader could sometimes be able to autoload `Foo::Wadus` in
334+
335+
```ruby
336+
class Foo::Bar
337+
Wadus
338+
end
339+
```
340+
341+
That does not match Ruby semantics because `Foo` is not in the nesting, and won't work at all in `zeitwerk` mode. If you find such corner case you can use the qualified name `Foo::Wadus`:
342+
343+
```ruby
344+
class Foo::Bar
345+
Foo::Wadus
346+
end
347+
```
348+
349+
or add `Foo` to the nesting:
350+
351+
```ruby
352+
module Foo
353+
class Bar
354+
Wadus
355+
end
356+
end
357+
```
358+
359+
360+
Thread-safety
361+
-------------
362+
363+
In classic mode, constant autoloading is not thread-safe, though Rails has locks in place for example to make web requests thread-safe.
364+
365+
Constant autoloading is thread-safe in `zeitwerk` mode. For example, you can now autoload in multi-threaded scripts executed by the `runner` command.
366+
367+
368+
Globs in `config.autoload_paths`
369+
--------------------------------
370+
371+
Beware of configurations like
372+
373+
```ruby
374+
config.autoload_paths += Dir["#{config.root}/lib/**/"]
375+
```
376+
377+
Every element of `config.autoload_paths` should represent the top-level namespace (`Object`) and they cannot be nested in consequence (with the exception of `concerns` directories explained above).
378+
379+
To fix this, just remove the wildcards:
380+
381+
```ruby
382+
config.autoload_paths << "#{config.root}/lib"
383+
```
384+
385+
386+
Eager loading and autoloading are consistent
387+
--------------------------------------------
388+
389+
In `classic` mode, if `app/models/foo.rb` defines `Bar`, you won't be able to autoload that file, but eager loading will work because it loads files recursively blindly. This can be a source of errors if you test things first eager loading, execution may fail later autoloading.
390+
391+
In `zeitwerk` mode both loading modes are consistent, they fail and err in the same files.

0 commit comments

Comments
 (0)