zeitwerk 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +325 -60
- data/lib/zeitwerk/autoloads.rb +71 -0
- data/lib/zeitwerk/error.rb +2 -0
- data/lib/zeitwerk/explicit_namespace.rb +20 -11
- data/lib/zeitwerk/gem_inflector.rb +2 -4
- data/lib/zeitwerk/inflector.rb +3 -6
- data/lib/zeitwerk/kernel.rb +8 -7
- data/lib/zeitwerk/loader/callbacks.rb +35 -14
- data/lib/zeitwerk/loader/config.rb +321 -0
- data/lib/zeitwerk/loader/helpers.rb +97 -0
- data/lib/zeitwerk/loader.rb +144 -464
- data/lib/zeitwerk/real_mod_name.rb +3 -2
- data/lib/zeitwerk/registry.rb +28 -30
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +13 -0
- metadata +10 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c85bb1028371436b80651c897670fef7158dcca7a916b10f6cb8584684c44442
|
|
4
|
+
data.tar.gz: c07fec548ab787daa28b10260beb022d7375aff90a506daa9bfecdc2f5bd6e5c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d01e5091fa868cdc80c4dd6eb82cc3ace4e21e0704aa2128e0ef5098a52140884daa62e9da862490104e31d393e06b33e29170e6d3f6fd3e44aa5111b25a06f1
|
|
7
|
+
data.tar.gz: 055d861fe2440f465277fbfce4c2bd9b7460d38f14dc5435449c6655d1be97f7e24fb7154b64102267c020e45d52f0bff21fbc7fc35eec0063b2dac2a92197bd
|
data/README.md
CHANGED
|
@@ -3,44 +3,61 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
[](https://rubygems.org/gems/zeitwerk)
|
|
6
|
-
[](https://github.com/fxn/zeitwerk/actions?query=event%3Apush)
|
|
7
7
|
|
|
8
8
|
<!-- TOC -->
|
|
9
9
|
|
|
10
10
|
- [Introduction](#introduction)
|
|
11
11
|
- [Synopsis](#synopsis)
|
|
12
12
|
- [File structure](#file-structure)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
- [The idea: File paths match constant paths](#the-idea-file-paths-match-constant-paths)
|
|
14
|
+
- [Inner simple constants](#inner-simple-constants)
|
|
15
|
+
- [Root directories and root namespaces](#root-directories-and-root-namespaces)
|
|
16
|
+
- [The default root namespace is `Object`](#the-default-root-namespace-is-object)
|
|
17
|
+
- [Custom root namespaces](#custom-root-namespaces)
|
|
16
18
|
- [Nested root directories](#nested-root-directories)
|
|
19
|
+
- [Implicit namespaces](#implicit-namespaces)
|
|
20
|
+
- [Explicit namespaces](#explicit-namespaces)
|
|
21
|
+
- [Collapsing directories](#collapsing-directories)
|
|
22
|
+
- [Testing compliance](#testing-compliance)
|
|
17
23
|
- [Usage](#usage)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- [
|
|
35
|
-
|
|
36
|
-
- [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
- [Setup](#setup)
|
|
25
|
+
- [Generic](#generic)
|
|
26
|
+
- [for_gem](#for_gem)
|
|
27
|
+
- [Autoloading](#autoloading)
|
|
28
|
+
- [Eager loading](#eager-loading)
|
|
29
|
+
- [Eager load exclusions](#eager-load-exclusions)
|
|
30
|
+
- [Global eager load](#global-eager-load)
|
|
31
|
+
- [Reloading](#reloading)
|
|
32
|
+
- [Inflection](#inflection)
|
|
33
|
+
- [Zeitwerk::Inflector](#zeitwerkinflector)
|
|
34
|
+
- [Zeitwerk::GemInflector](#zeitwerkgeminflector)
|
|
35
|
+
- [Custom inflector](#custom-inflector)
|
|
36
|
+
- [Callbacks](#callbacks)
|
|
37
|
+
- [The on_setup callback](#the-on_setup-callback)
|
|
38
|
+
- [The on_load callback](#the-on_load-callback)
|
|
39
|
+
- [The on_unload callback](#the-on_unload-callback)
|
|
40
|
+
- [Technical details](#technical-details)
|
|
41
|
+
- [Logging](#logging)
|
|
42
|
+
- [Loader tag](#loader-tag)
|
|
43
|
+
- [Ignoring parts of the project](#ignoring-parts-of-the-project)
|
|
44
|
+
- [Use case: Files that do not follow the conventions](#use-case-files-that-do-not-follow-the-conventions)
|
|
45
|
+
- [Use case: The adapter pattern](#use-case-the-adapter-pattern)
|
|
46
|
+
- [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
|
|
47
|
+
- [Edge cases](#edge-cases)
|
|
48
|
+
- [Beware of circular dependencies](#beware-of-circular-dependencies)
|
|
49
|
+
- [Reopening third-party namespaces](#reopening-third-party-namespaces)
|
|
50
|
+
- [Rules of thumb](#rules-of-thumb)
|
|
51
|
+
- [Debuggers](#debuggers)
|
|
52
|
+
- [debug.rb](#debugrb)
|
|
53
|
+
- [Break](#break)
|
|
54
|
+
- [Byebug](#byebug)
|
|
40
55
|
- [Pronunciation](#pronunciation)
|
|
41
56
|
- [Supported Ruby versions](#supported-ruby-versions)
|
|
42
57
|
- [Testing](#testing)
|
|
43
58
|
- [Motivation](#motivation)
|
|
59
|
+
- [Kernel#require is brittle](#kernelrequire-is-brittle)
|
|
60
|
+
- [Rails autoloading was brittle](#rails-autoloading-was-brittle)
|
|
44
61
|
- [Thanks](#thanks)
|
|
45
62
|
- [License](#license)
|
|
46
63
|
|
|
@@ -116,6 +133,9 @@ Zeitwerk::Loader.eager_load_all
|
|
|
116
133
|
<a id="markdown-file-structure" name="file-structure"></a>
|
|
117
134
|
## File structure
|
|
118
135
|
|
|
136
|
+
<a id="markdown-the-idea-file-paths-match-constant-paths" name="the-idea-file-paths-match-constant-paths"></a>
|
|
137
|
+
### The idea: File paths match constant paths
|
|
138
|
+
|
|
119
139
|
To have a file structure Zeitwerk can work with, just name files and directories after the name of the classes and modules they define:
|
|
120
140
|
|
|
121
141
|
```
|
|
@@ -125,25 +145,67 @@ lib/my_gem/bar_baz.rb -> MyGem::BarBaz
|
|
|
125
145
|
lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
|
|
126
146
|
```
|
|
127
147
|
|
|
128
|
-
|
|
148
|
+
You can tune that a bit by [collapsing directories](#collapsing-directories), or by [ignoring parts of the project](#ignoring-parts-of-the-project), but that is the main idea.
|
|
149
|
+
|
|
150
|
+
<a id="markdown-inner-simple-constants" name="inner-simple-constants"></a>
|
|
151
|
+
### Inner simple constants
|
|
152
|
+
|
|
153
|
+
While a simple constant like `HttpCrawler::MAX_RETRIES` can be defined in its own file:
|
|
129
154
|
|
|
130
155
|
```ruby
|
|
131
|
-
|
|
132
|
-
|
|
156
|
+
# http_crawler/max_retries.rb
|
|
157
|
+
HttpCrawler::MAX_RETRIES = 10
|
|
133
158
|
```
|
|
134
159
|
|
|
135
|
-
|
|
160
|
+
that is not required, you can also define it the regular way:
|
|
136
161
|
|
|
162
|
+
```ruby
|
|
163
|
+
# http_crawler.rb
|
|
164
|
+
class HttpCrawler
|
|
165
|
+
MAX_RETRIES = 10
|
|
166
|
+
end
|
|
137
167
|
```
|
|
138
|
-
|
|
139
|
-
|
|
168
|
+
|
|
169
|
+
The first example needs a custom [inflection](https://github.com/fxn/zeitwerk#inflection) rule:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
loader.inflector.inflect("max_retries" => "MAX_RETRIES")
|
|
140
173
|
```
|
|
141
174
|
|
|
142
|
-
|
|
175
|
+
Otherwise, Zeitwerk would expect the file to define `MaxRetries`.
|
|
176
|
+
|
|
177
|
+
In the second example, no custom rule is needed.
|
|
143
178
|
|
|
144
|
-
|
|
179
|
+
<a id="markdown-root-directories-and-root-namespaces" name="root-directories-and-root-namespaces"></a>
|
|
180
|
+
### Root directories and root namespaces
|
|
145
181
|
|
|
146
|
-
|
|
182
|
+
Every directory configured with `push_dir` is called a _root directory_, and they represent _root namespaces_.
|
|
183
|
+
|
|
184
|
+
<a id="markdown-the-default-root-namespace-is-object" name="the-default-root-namespace-is-object"></a>
|
|
185
|
+
#### The default root namespace is `Object`
|
|
186
|
+
|
|
187
|
+
By default, the namespace associated to a root directory is the top-level one: `Object`.
|
|
188
|
+
|
|
189
|
+
For example, given
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
loader.push_dir("#{__dir__}/models")
|
|
193
|
+
loader.push_dir("#{__dir__}/serializers"))
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
these are the expected classes and modules being defined by these files:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
models/user.rb -> User
|
|
200
|
+
serializers/user_serializer.rb -> UserSerializer
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
<a id="markdown-custom-root-namespaces" name="custom-root-namespaces"></a>
|
|
204
|
+
#### Custom root namespaces
|
|
205
|
+
|
|
206
|
+
While `Object` is by far the most common root namespace, you can associate a different one to a particular root directory. The method `push_dir` accepts a class or module object in the optional `namespace` keyword argument.
|
|
207
|
+
|
|
208
|
+
For example, given:
|
|
147
209
|
|
|
148
210
|
```ruby
|
|
149
211
|
require "active_job"
|
|
@@ -151,9 +213,26 @@ require "active_job/queue_adapters"
|
|
|
151
213
|
loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
|
|
152
214
|
```
|
|
153
215
|
|
|
154
|
-
|
|
216
|
+
a file defining `ActiveJob::QueueAdapters::MyQueueAdapter` does not need the conventional parent directories, you can (and have to) store the file directly below `adapters`:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
adapters/my_queue_adapter.rb -> ActiveJob::QueueAdapters::MyQueueAdapter
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Please, note that the given root namespace must be non-reloadable, though autoloaded constants in that namespace can be. That is, if you associate `app/api` with an existing `Api` module, that module should not be reloadable. However, if the project defines and autoloads the class `Api::Deliveries`, that one can be reloaded.
|
|
223
|
+
|
|
224
|
+
<a id="markdown-nested-root-directories" name="nested-root-directories"></a>
|
|
225
|
+
#### Nested root directories
|
|
155
226
|
|
|
156
|
-
|
|
227
|
+
Root directories should not be ideally nested, but Zeitwerk supports them because in Rails, for example, both `app/models` and `app/models/concerns` belong to the autoload paths.
|
|
228
|
+
|
|
229
|
+
Zeitwerk detects nested root directories, and treats them as roots only. In the example above, `concerns` is not considered to be a namespace below `app/models`. For example, the file:
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
app/models/concerns/geolocatable.rb
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
should define `Geolocatable`, not `Concerns::Geolocatable`.
|
|
157
236
|
|
|
158
237
|
<a id="markdown-implicit-namespaces" name="implicit-namespaces"></a>
|
|
159
238
|
### Implicit namespaces
|
|
@@ -202,7 +281,7 @@ booking/actions/create.rb -> Booking::Create
|
|
|
202
281
|
To make it work that way, configure Zeitwerk to collapse said directory:
|
|
203
282
|
|
|
204
283
|
```ruby
|
|
205
|
-
loader.collapse("booking/actions")
|
|
284
|
+
loader.collapse("#{__dir__}/booking/actions")
|
|
206
285
|
```
|
|
207
286
|
|
|
208
287
|
This method accepts an arbitrary number of strings or `Pathname` objects, and also an array of them.
|
|
@@ -212,21 +291,25 @@ You can pass directories and glob patterns. Glob patterns are expanded when they
|
|
|
212
291
|
To illustrate usage of glob patterns, if `actions` in the example above is part of a standardized structure, you could use a wildcard:
|
|
213
292
|
|
|
214
293
|
```ruby
|
|
215
|
-
loader.collapse("
|
|
294
|
+
loader.collapse("#{__dir__}/*/actions")
|
|
216
295
|
```
|
|
217
296
|
|
|
218
|
-
<a id="markdown-
|
|
219
|
-
###
|
|
297
|
+
<a id="markdown-testing-compliance" name="testing-compliance"></a>
|
|
298
|
+
### Testing compliance
|
|
220
299
|
|
|
221
|
-
|
|
300
|
+
When a managed file is loaded, Zeitwerk verifies the expected constant is defined. If it is not, `Zeitwerk::NameError` is raised.
|
|
222
301
|
|
|
223
|
-
|
|
302
|
+
So, an easy way to ensure compliance in the test suite is to eager load the project:
|
|
224
303
|
|
|
304
|
+
```ruby
|
|
305
|
+
begin
|
|
306
|
+
loader.eager_load(force: true)
|
|
307
|
+
rescue Zeitwerk::NameError => e
|
|
308
|
+
flunk e.message
|
|
309
|
+
else
|
|
310
|
+
assert true
|
|
311
|
+
end
|
|
225
312
|
```
|
|
226
|
-
app/models/concerns/geolocatable.rb
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
should define `Geolocatable`, not `Concerns::Geolocatable`.
|
|
230
313
|
|
|
231
314
|
<a id="markdown-usage" name="usage"></a>
|
|
232
315
|
## Usage
|
|
@@ -330,7 +413,16 @@ Zeitwerk instances are able to eager load their managed files:
|
|
|
330
413
|
loader.eager_load
|
|
331
414
|
```
|
|
332
415
|
|
|
333
|
-
That skips [ignored files and directories](#ignoring-parts-of-the-project)
|
|
416
|
+
That skips [ignored files and directories](#ignoring-parts-of-the-project).
|
|
417
|
+
|
|
418
|
+
In gems, the method needs to be invoked after the main namespace has been defined, as shown in [Synopsis](https://github.com/fxn/zeitwerk#synopsis).
|
|
419
|
+
|
|
420
|
+
Eager loading is synchronized and idempotent.
|
|
421
|
+
|
|
422
|
+
<a id="markdown-eager-load-exclusions" name="eager-load-exclusions"></a>
|
|
423
|
+
#### Eager load exclusions
|
|
424
|
+
|
|
425
|
+
You can tell Zeitwerk that certain files or directories are autoloadable, but should not be eager loaded:
|
|
334
426
|
|
|
335
427
|
```ruby
|
|
336
428
|
db_adapters = "#{__dir__}/my_gem/db_adapters"
|
|
@@ -339,13 +431,20 @@ loader.setup
|
|
|
339
431
|
loader.eager_load # won't eager load the database adapters
|
|
340
432
|
```
|
|
341
433
|
|
|
342
|
-
|
|
434
|
+
However, that can be overridden with `force`:
|
|
343
435
|
|
|
344
|
-
|
|
436
|
+
```ruby
|
|
437
|
+
loader.eager_load(force: true) # database adapters are eager loaded
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Which may be handy if the project eager loads in the test suite to [ensure project layour compliance](#testing-compliance).
|
|
345
441
|
|
|
346
|
-
|
|
442
|
+
The `force` flag does not affect ignored files and directories, those are still ignored.
|
|
347
443
|
|
|
348
|
-
|
|
444
|
+
<a id="markdown-global-eager-load" name="global-eager-load"></a>
|
|
445
|
+
#### Global eager load
|
|
446
|
+
|
|
447
|
+
If you want to eager load yourself and all dependencies that use Zeitwerk, you can broadcast the `eager_load` call to all instances:
|
|
349
448
|
|
|
350
449
|
```ruby
|
|
351
450
|
Zeitwerk::Loader.eager_load_all
|
|
@@ -355,6 +454,8 @@ This may be handy in top-level services, like web applications.
|
|
|
355
454
|
|
|
356
455
|
Note that thanks to idempotence `Zeitwerk::Loader.eager_load_all` won't eager load twice if any of the instances already eager loaded.
|
|
357
456
|
|
|
457
|
+
This method does not accept the `force` flag, since in general it wouldn't be a good idea to force eager loading in 3rd party code.
|
|
458
|
+
|
|
358
459
|
<a id="markdown-reloading" name="reloading"></a>
|
|
359
460
|
### Reloading
|
|
360
461
|
|
|
@@ -386,11 +487,13 @@ On reloading, client code has to update anything that would otherwise be storing
|
|
|
386
487
|
<a id="markdown-inflection" name="inflection"></a>
|
|
387
488
|
### Inflection
|
|
388
489
|
|
|
389
|
-
Each individual loader needs an inflector to figure out which constant path would a given file or directory map to. Zeitwerk ships with two basic inflectors.
|
|
490
|
+
Each individual loader needs an inflector to figure out which constant path would a given file or directory map to. Zeitwerk ships with two basic inflectors, and you can define your own.
|
|
390
491
|
|
|
391
492
|
<a id="markdown-zeitwerkinflector" name="zeitwerkinflector"></a>
|
|
392
493
|
#### Zeitwerk::Inflector
|
|
393
494
|
|
|
495
|
+
Each loader instantiated with `Zeitwerk::Loader.new` has an inflector of this type by default.
|
|
496
|
+
|
|
394
497
|
This is a very basic inflector that converts snake case to camel case:
|
|
395
498
|
|
|
396
499
|
```
|
|
@@ -417,16 +520,16 @@ loader.inflector.inflect "mysql_adapter" => "MySQLAdapter"
|
|
|
417
520
|
|
|
418
521
|
Overrides need to be configured before calling `setup`.
|
|
419
522
|
|
|
420
|
-
There are no inflection rules or global configuration that can affect this inflector. It is deterministic.
|
|
421
|
-
|
|
422
|
-
Loaders instantiated with `Zeitwerk::Loader.new` have an inflector of this type, independent of each other.
|
|
523
|
+
The inflectors of different loaders are independent of each other. There are no global inflection rules or global configuration that can affect this inflector. It is deterministic.
|
|
423
524
|
|
|
424
525
|
<a id="markdown-zeitwerkgeminflector" name="zeitwerkgeminflector"></a>
|
|
425
526
|
#### Zeitwerk::GemInflector
|
|
426
527
|
|
|
528
|
+
Each loader instantiated with `Zeitwerk::Loader.for_gem` has an inflector of this type by default.
|
|
529
|
+
|
|
427
530
|
This inflector is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`.
|
|
428
531
|
|
|
429
|
-
|
|
532
|
+
The inflectors of different loaders are independent of each other. There are no global inflection rules or global configuration that can affect this inflector. It is deterministic.
|
|
430
533
|
|
|
431
534
|
<a id="markdown-custom-inflector" name="custom-inflector"></a>
|
|
432
535
|
#### Custom inflector
|
|
@@ -502,6 +605,129 @@ class MyGem::Inflector < Zeitwerk::GemInflector
|
|
|
502
605
|
end
|
|
503
606
|
```
|
|
504
607
|
|
|
608
|
+
<a id="markdown-callbacks" name="callbacks"></a>
|
|
609
|
+
### Callbacks
|
|
610
|
+
|
|
611
|
+
<a id="markdown-the-on_setup-callback" name="the-on_setup-callback"></a>
|
|
612
|
+
#### The on_setup callback
|
|
613
|
+
|
|
614
|
+
The `on_setup` callback is fired on setup and on each reload:
|
|
615
|
+
|
|
616
|
+
```ruby
|
|
617
|
+
loader.on_setup do
|
|
618
|
+
# Ready to autoload here.
|
|
619
|
+
end
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Multiple `on_setup` callbacks are supported, and they run in order of definition.
|
|
623
|
+
|
|
624
|
+
If `setup` was already executed, the callback is fired immediately.
|
|
625
|
+
|
|
626
|
+
<a id="markdown-the-on_load-callback" name="the-on_load-callback"></a>
|
|
627
|
+
#### The on_load callback
|
|
628
|
+
|
|
629
|
+
The usual place to run something when a file is loaded is the file itself. However, sometimes you'd like to be called, and this is possible with the `on_load` callback.
|
|
630
|
+
|
|
631
|
+
For example, let's imagine this class belongs to a Rails application:
|
|
632
|
+
|
|
633
|
+
```ruby
|
|
634
|
+
class SomeApiClient
|
|
635
|
+
class << self
|
|
636
|
+
attr_accessor :endpoint
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
With `on_load`, it is easy to schedule code at boot time that initializes `endpoint` according to the configuration:
|
|
642
|
+
|
|
643
|
+
```ruby
|
|
644
|
+
# config/environments/development.rb
|
|
645
|
+
loader.on_load("SomeApiClient") do |klass, _abspath|
|
|
646
|
+
klass.endpoint = "https://api.dev"
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# config/environments/production.rb
|
|
650
|
+
loader.on_load("SomeApiClient") do |klass, _abspath|
|
|
651
|
+
klass.endpoint = "https://api.prod"
|
|
652
|
+
end
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
Some uses cases:
|
|
656
|
+
|
|
657
|
+
* Doing something with a reloadable class or module in a Rails application during initialization, in a way that plays well with reloading. As in the previous example.
|
|
658
|
+
* Delaying the execution of the block until the class is loaded for performance.
|
|
659
|
+
* Delaying the execution of the block until the class is loaded because it follows the adapter pattern and better not to load the class if the user does not need it.
|
|
660
|
+
|
|
661
|
+
`on_load` gets a target constant path as a string (e.g., "User", or "Service::NotificationsGateway"). When fired, its block receives the stored value, and the absolute path to the corresponding file or directory as a string. The callback is executed every time the target is loaded. That includes reloads.
|
|
662
|
+
|
|
663
|
+
Multiple callbacks on the same target are supported, and they run in order of definition.
|
|
664
|
+
|
|
665
|
+
The block is executed once the loader has loaded the target. In particular, if the target was already loaded when the callback is defined, the block won't run. But if you reload and load the target again, then it will. Normally, you'll want to define `on_load` callbacks before `setup`.
|
|
666
|
+
|
|
667
|
+
Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
|
|
668
|
+
|
|
669
|
+
It is also possible to be called when any constant managed by the loader is loaded:
|
|
670
|
+
|
|
671
|
+
```ruby
|
|
672
|
+
loader.on_load do |cpath, value, abspath|
|
|
673
|
+
# ...
|
|
674
|
+
end
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
The block gets the constant path as a string (e.g., "User", or "Foo::VERSION"), the value it stores (e.g., the class object stored in `User`, or "2.5.0"), and the absolute path to the corresponding file or directory as a string.
|
|
678
|
+
|
|
679
|
+
Multiple callbacks like these are supported, and they run in order of definition.
|
|
680
|
+
|
|
681
|
+
There are use cases for this last catch-all callback, but they are rare. If you just need to understand how things are being loaded for debugging purposes, please remember that `Zeitwerk::Loader#log!` logs plenty of information.
|
|
682
|
+
|
|
683
|
+
If both types of callbacks are defined, the specific ones run first.
|
|
684
|
+
|
|
685
|
+
Since `on_load` callbacks are executed right after files are loaded, even if the loading context seems to be far away, in practice **the block is subject to [circular dependencies](#beware-of-circular-dependencies)**. As a rule of thumb, as far as loading order and its interdependencies is concerned, you have to program as if the block was executed at the bottom of the file just loaded.
|
|
686
|
+
|
|
687
|
+
<a id="markdown-the-on_unload-callback" name="the-on_unload-callback"></a>
|
|
688
|
+
#### The on_unload callback
|
|
689
|
+
|
|
690
|
+
When reloading is enabled, you may occasionally need to execute something before a certain autoloaded class or module is unloaded. The `on_unload` callback allows you to do that.
|
|
691
|
+
|
|
692
|
+
For example, let's imagine that a `Country` class fetches a list of countries and caches them when it is loaded. You might want to clear that cache if unloaded:
|
|
693
|
+
|
|
694
|
+
```ruby
|
|
695
|
+
loader.on_unload("Country") do |klass, _abspath|
|
|
696
|
+
klass.clear_cache
|
|
697
|
+
end
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
`on_unload` gets a target constant path as a string (e.g., "User", or "Service::NotificationsGateway"). When fired, its block receives the stored value, and the absolute path to the corresponding file or directory as a string. The callback is executed every time the target is unloaded.
|
|
701
|
+
|
|
702
|
+
`on_unload` blocks are executed before the class is unloaded, but in the middle of unloading, which happens in an unspecified order. Therefore, **that callback should not refer to any reloadable constant because there is no guarantee the constant works there**. Those blocks should rely on objects only, as in the example above, or regular constants not managed by the loader. This remark is transitive, applies to any methods invoked within the block.
|
|
703
|
+
|
|
704
|
+
Multiple callbacks on the same target are supported, and they run in order of definition.
|
|
705
|
+
|
|
706
|
+
Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
|
|
707
|
+
|
|
708
|
+
It is also possible to be called when any constant managed by the loader is unloaded:
|
|
709
|
+
|
|
710
|
+
```ruby
|
|
711
|
+
loader.on_unload do |cpath, value, abspath|
|
|
712
|
+
# ...
|
|
713
|
+
end
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
The block gets the constant path as a string (e.g., "User", or "Foo::VERSION"), the value it stores (e.g., the class object stored in `User`, or "2.5.0"), and the absolute path to the corresponding file or directory as a string.
|
|
717
|
+
|
|
718
|
+
Multiple callbacks like these are supported, and they run in order of definition.
|
|
719
|
+
|
|
720
|
+
If both types of callbacks are defined, the specific ones run first.
|
|
721
|
+
|
|
722
|
+
<a id="markdown-technical-details" name="technical-details"></a>
|
|
723
|
+
#### Technical details
|
|
724
|
+
|
|
725
|
+
Zeitwerk uses the word "unload" to ease communication and for symmetry with `on_load`. However, in Ruby you cannot unload things for real. So, when does `on_unload` technically happen?
|
|
726
|
+
|
|
727
|
+
When unloading, Zeitwerk issues `Module#remove_const` calls. Classes and modules are no longer reachable through their constants, and `on_unload` callbacks are executed right before those calls.
|
|
728
|
+
|
|
729
|
+
Technically, though, the objects themselves are still alive, but if everything is used as expected and they are not stored in any non-reloadable place (don't do that), they are ready for garbage collection, which is when the real unloading happens.
|
|
730
|
+
|
|
505
731
|
<a id="markdown-logging" name="logging"></a>
|
|
506
732
|
### Logging
|
|
507
733
|
|
|
@@ -670,6 +896,26 @@ Trip = Struct.new { ... } # NOT SUPPORTED
|
|
|
670
896
|
|
|
671
897
|
This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
|
|
672
898
|
|
|
899
|
+
<a id="markdown-beware-of-circular-dependencies" name="beware-of-circular-dependencies"></a>
|
|
900
|
+
### Beware of circular dependencies
|
|
901
|
+
|
|
902
|
+
In Ruby, you can't have certain top-level circular dependencies. Take for example:
|
|
903
|
+
|
|
904
|
+
```ruby
|
|
905
|
+
# c.rb
|
|
906
|
+
class C < D
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
# d.rb
|
|
910
|
+
class D
|
|
911
|
+
C
|
|
912
|
+
end
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
In order to define `C`, you need to load `D`. However, the body of `D` refers to `C`.
|
|
916
|
+
|
|
917
|
+
Circular dependencies like those do not work in plain Ruby, and therefore do not work in projects managed by Zeitwerk either.
|
|
918
|
+
|
|
673
919
|
<a id="markdown-reopening-third-party-namespaces" name="reopening-third-party-namespaces"></a>
|
|
674
920
|
### Reopening third-party namespaces
|
|
675
921
|
|
|
@@ -724,6 +970,11 @@ With that, when Zeitwerk scans the file system and reaches the gem directories `
|
|
|
724
970
|
<a id="markdown-debuggers" name="debuggers"></a>
|
|
725
971
|
### Debuggers
|
|
726
972
|
|
|
973
|
+
<a id="markdown-debugrb" name="debugrb"></a>
|
|
974
|
+
#### debug.rb
|
|
975
|
+
|
|
976
|
+
The new [debug.rb](https://github.com/ruby/debug) gem and Zeitwerk seem to be compatible, as far as I can tell. This is the new debugger that is going to ship with Ruby 3.1.
|
|
977
|
+
|
|
727
978
|
<a id="markdown-break" name="break"></a>
|
|
728
979
|
#### Break
|
|
729
980
|
|
|
@@ -742,7 +993,11 @@ Zeitwerk and [Byebug](https://github.com/deivid-rodriguez/byebug) are incompatib
|
|
|
742
993
|
<a id="markdown-supported-ruby-versions" name="supported-ruby-versions"></a>
|
|
743
994
|
## Supported Ruby versions
|
|
744
995
|
|
|
745
|
-
Zeitwerk works with
|
|
996
|
+
Zeitwerk works with CRuby 2.5 and above.
|
|
997
|
+
|
|
998
|
+
On TruffleRuby all is good except for thread-safety. Right now, in TruffleRuby `Module#autoload` does not block threads accessing a constant that is being autoloaded. CRuby prevents such access to avoid concurrent threads from seeing partial evaluations of the corresponding file. Zeitwerk inherits autoloading thread-safety from this property. This is not an issue if your project gets eager loaded, or if you lazy load in single-threaded environments. (See https://github.com/oracle/truffleruby/issues/2431.)
|
|
999
|
+
|
|
1000
|
+
JRuby 9.3.0.0 is almost there. As of this writing, the test suite of Zeitwerk passes on JRuby except for three tests. (See https://github.com/jruby/jruby/issues/6781.)
|
|
746
1001
|
|
|
747
1002
|
<a id="markdown-testing" name="testing"></a>
|
|
748
1003
|
## Testing
|
|
@@ -773,9 +1028,19 @@ and run `bin/test`.
|
|
|
773
1028
|
<a id="markdown-motivation" name="motivation"></a>
|
|
774
1029
|
## Motivation
|
|
775
1030
|
|
|
776
|
-
|
|
1031
|
+
<a id="markdown-kernelrequire-is-brittle" name="kernelrequire-is-brittle"></a>
|
|
1032
|
+
### Kernel#require is brittle
|
|
1033
|
+
|
|
1034
|
+
Since `require` has global side-effects, and there is no static way to verify that you have issued the `require` calls for code that your file depends on, in practice it is very easy to forget some. That introduces bugs that depend on the load order.
|
|
1035
|
+
|
|
1036
|
+
Also, if the project has namespaces, setting things up and getting client code to load things in a consistent way needs discipline. For example, `require "foo/bar"` may define `Foo`, instead of reopen it. That may be a broken window, giving place to superclass mismatches or partially-defined namespaces.
|
|
1037
|
+
|
|
1038
|
+
With Zeitwerk, you just name things following conventions and done. Things are available everywhere, and descend is always orderly. Without effort and without broken windows.
|
|
1039
|
+
|
|
1040
|
+
<a id="markdown-rails-autoloading-was-brittle" name="rails-autoloading-was-brittle"></a>
|
|
1041
|
+
### Rails autoloading was brittle
|
|
777
1042
|
|
|
778
|
-
|
|
1043
|
+
Autoloading in Rails was based on `const_missing` up to Rails 5. That callback lacks fundamental information like the nesting or the resolution algorithm being used. Because of that, Rails autoloading was not able to match Ruby's semantics, and that introduced a [series of issues](https://guides.rubyonrails.org/v5.2/autoloading_and_reloading_constants.html#common-gotchas). Zeitwerk is based on a different technique and fixed Rails autoloading starting with Rails 6.
|
|
779
1044
|
|
|
780
1045
|
<a id="markdown-thanks" name="thanks"></a>
|
|
781
1046
|
## Thanks
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zeitwerk
|
|
4
|
+
# @private
|
|
5
|
+
class Autoloads
|
|
6
|
+
# Maps crefs for which an autoload has been defined to the corresponding
|
|
7
|
+
# absolute path.
|
|
8
|
+
#
|
|
9
|
+
# [Object, :User] => "/Users/fxn/blog/app/models/user.rb"
|
|
10
|
+
# [Object, :Hotel] => "/Users/fxn/blog/app/models/hotel"
|
|
11
|
+
# ...
|
|
12
|
+
#
|
|
13
|
+
# This colection is transient, callbacks delete its entries as autoloads get
|
|
14
|
+
# executed.
|
|
15
|
+
#
|
|
16
|
+
# @sig Hash[[Module, Symbol], String]
|
|
17
|
+
attr_reader :c2a
|
|
18
|
+
|
|
19
|
+
# This is the inverse of c2a, for inverse lookups.
|
|
20
|
+
#
|
|
21
|
+
# @sig Hash[String, [Module, Symbol]]
|
|
22
|
+
attr_reader :a2c
|
|
23
|
+
|
|
24
|
+
# @sig () -> void
|
|
25
|
+
def initialize
|
|
26
|
+
@c2a = {}
|
|
27
|
+
@a2c = {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @sig (Module, Symbol, String) -> void
|
|
31
|
+
def define(parent, cname, abspath)
|
|
32
|
+
parent.autoload(cname, abspath)
|
|
33
|
+
cref = [parent, cname]
|
|
34
|
+
c2a[cref] = abspath
|
|
35
|
+
a2c[abspath] = cref
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @sig () { () -> [[Module, Symbol], String] } -> void
|
|
39
|
+
def each(&block)
|
|
40
|
+
c2a.each(&block)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @sig (Module, Symbol) -> String?
|
|
44
|
+
def abspath_for(parent, cname)
|
|
45
|
+
c2a[[parent, cname]]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @sig (String) -> [Module, Symbol]?
|
|
49
|
+
def cref_for(abspath)
|
|
50
|
+
a2c[abspath]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @sig (String) -> [Module, Symbol]?
|
|
54
|
+
def delete(abspath)
|
|
55
|
+
cref = a2c.delete(abspath)
|
|
56
|
+
c2a.delete(cref)
|
|
57
|
+
cref
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @sig () -> void
|
|
61
|
+
def clear
|
|
62
|
+
c2a.clear
|
|
63
|
+
a2c.clear
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @sig () -> bool
|
|
67
|
+
def empty?
|
|
68
|
+
c2a.empty? && a2c.empty?
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|