zeitwerk 2.4.2 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +287 -69
- data/lib/zeitwerk/autoloads.rb +71 -0
- data/lib/zeitwerk/error.rb +2 -0
- data/lib/zeitwerk/explicit_namespace.rb +11 -3
- data/lib/zeitwerk/kernel.rb +6 -5
- data/lib/zeitwerk/loader/callbacks.rb +18 -12
- data/lib/zeitwerk/loader/config.rb +321 -0
- data/lib/zeitwerk/loader/helpers.rb +97 -0
- data/lib/zeitwerk/loader.rb +114 -419
- data/lib/zeitwerk/real_mod_name.rb +2 -0
- data/lib/zeitwerk/registry.rb +16 -7
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +13 -0
- metadata +7 -4
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,45 +3,61 @@
|
|
3
3
|
|
4
4
|
|
5
5
|
[![Gem Version](https://img.shields.io/gem/v/zeitwerk.svg?style=for-the-badge)](https://rubygems.org/gems/zeitwerk)
|
6
|
-
[![Build Status](https://img.shields.io/
|
6
|
+
[![Build Status](https://img.shields.io/github/workflow/status/fxn/zeitwerk/CI?event=push&style=for-the-badge)](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
|
-
|
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)
|
28
38
|
- [The on_load callback](#the-on_load-callback)
|
29
|
-
- [
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
- [
|
36
|
-
- [
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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)
|
41
55
|
- [Pronunciation](#pronunciation)
|
42
56
|
- [Supported Ruby versions](#supported-ruby-versions)
|
43
57
|
- [Testing](#testing)
|
44
58
|
- [Motivation](#motivation)
|
59
|
+
- [Kernel#require is brittle](#kernelrequire-is-brittle)
|
60
|
+
- [Rails autoloading was brittle](#rails-autoloading-was-brittle)
|
45
61
|
- [Thanks](#thanks)
|
46
62
|
- [License](#license)
|
47
63
|
|
@@ -117,6 +133,9 @@ Zeitwerk::Loader.eager_load_all
|
|
117
133
|
<a id="markdown-file-structure" name="file-structure"></a>
|
118
134
|
## File structure
|
119
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
|
+
|
120
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:
|
121
140
|
|
122
141
|
```
|
@@ -126,25 +145,67 @@ lib/my_gem/bar_baz.rb -> MyGem::BarBaz
|
|
126
145
|
lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
|
127
146
|
```
|
128
147
|
|
129
|
-
|
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:
|
130
154
|
|
131
155
|
```ruby
|
132
|
-
|
133
|
-
|
156
|
+
# http_crawler/max_retries.rb
|
157
|
+
HttpCrawler::MAX_RETRIES = 10
|
134
158
|
```
|
135
159
|
|
136
|
-
|
160
|
+
that is not required, you can also define it the regular way:
|
137
161
|
|
162
|
+
```ruby
|
163
|
+
# http_crawler.rb
|
164
|
+
class HttpCrawler
|
165
|
+
MAX_RETRIES = 10
|
166
|
+
end
|
138
167
|
```
|
139
|
-
|
140
|
-
|
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")
|
173
|
+
```
|
174
|
+
|
175
|
+
Otherwise, Zeitwerk would expect the file to define `MaxRetries`.
|
176
|
+
|
177
|
+
In the second example, no custom rule is needed.
|
178
|
+
|
179
|
+
<a id="markdown-root-directories-and-root-namespaces" name="root-directories-and-root-namespaces"></a>
|
180
|
+
### Root directories and root namespaces
|
181
|
+
|
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
|
141
201
|
```
|
142
202
|
|
143
|
-
|
203
|
+
<a id="markdown-custom-root-namespaces" name="custom-root-namespaces"></a>
|
204
|
+
#### Custom root namespaces
|
144
205
|
|
145
|
-
|
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.
|
146
207
|
|
147
|
-
|
208
|
+
For example, given:
|
148
209
|
|
149
210
|
```ruby
|
150
211
|
require "active_job"
|
@@ -152,9 +213,26 @@ require "active_job/queue_adapters"
|
|
152
213
|
loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
|
153
214
|
```
|
154
215
|
|
155
|
-
|
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
|
226
|
+
|
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
|
+
```
|
156
234
|
|
157
|
-
|
235
|
+
should define `Geolocatable`, not `Concerns::Geolocatable`.
|
158
236
|
|
159
237
|
<a id="markdown-implicit-namespaces" name="implicit-namespaces"></a>
|
160
238
|
### Implicit namespaces
|
@@ -216,18 +294,22 @@ To illustrate usage of glob patterns, if `actions` in the example above is part
|
|
216
294
|
loader.collapse("#{__dir__}/*/actions")
|
217
295
|
```
|
218
296
|
|
219
|
-
<a id="markdown-
|
220
|
-
###
|
297
|
+
<a id="markdown-testing-compliance" name="testing-compliance"></a>
|
298
|
+
### Testing compliance
|
221
299
|
|
222
|
-
|
300
|
+
When a managed file is loaded, Zeitwerk verifies the expected constant is defined. If it is not, `Zeitwerk::NameError` is raised.
|
223
301
|
|
224
|
-
|
302
|
+
So, an easy way to ensure compliance in the test suite is to eager load the project:
|
225
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
|
226
312
|
```
|
227
|
-
app/models/concerns/geolocatable.rb
|
228
|
-
```
|
229
|
-
|
230
|
-
should define `Geolocatable`, not `Concerns::Geolocatable`.
|
231
313
|
|
232
314
|
<a id="markdown-usage" name="usage"></a>
|
233
315
|
## Usage
|
@@ -331,7 +413,16 @@ Zeitwerk instances are able to eager load their managed files:
|
|
331
413
|
loader.eager_load
|
332
414
|
```
|
333
415
|
|
334
|
-
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:
|
335
426
|
|
336
427
|
```ruby
|
337
428
|
db_adapters = "#{__dir__}/my_gem/db_adapters"
|
@@ -340,13 +431,20 @@ loader.setup
|
|
340
431
|
loader.eager_load # won't eager load the database adapters
|
341
432
|
```
|
342
433
|
|
343
|
-
|
434
|
+
However, that can be overridden with `force`:
|
344
435
|
|
345
|
-
|
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).
|
346
441
|
|
347
|
-
|
442
|
+
The `force` flag does not affect ignored files and directories, those are still ignored.
|
348
443
|
|
349
|
-
|
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:
|
350
448
|
|
351
449
|
```ruby
|
352
450
|
Zeitwerk::Loader.eager_load_all
|
@@ -356,6 +454,8 @@ This may be handy in top-level services, like web applications.
|
|
356
454
|
|
357
455
|
Note that thanks to idempotence `Zeitwerk::Loader.eager_load_all` won't eager load twice if any of the instances already eager loaded.
|
358
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
|
+
|
359
459
|
<a id="markdown-reloading" name="reloading"></a>
|
360
460
|
### Reloading
|
361
461
|
|
@@ -387,11 +487,13 @@ On reloading, client code has to update anything that would otherwise be storing
|
|
387
487
|
<a id="markdown-inflection" name="inflection"></a>
|
388
488
|
### Inflection
|
389
489
|
|
390
|
-
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.
|
391
491
|
|
392
492
|
<a id="markdown-zeitwerkinflector" name="zeitwerkinflector"></a>
|
393
493
|
#### Zeitwerk::Inflector
|
394
494
|
|
495
|
+
Each loader instantiated with `Zeitwerk::Loader.new` has an inflector of this type by default.
|
496
|
+
|
395
497
|
This is a very basic inflector that converts snake case to camel case:
|
396
498
|
|
397
499
|
```
|
@@ -418,16 +520,16 @@ loader.inflector.inflect "mysql_adapter" => "MySQLAdapter"
|
|
418
520
|
|
419
521
|
Overrides need to be configured before calling `setup`.
|
420
522
|
|
421
|
-
There are no inflection rules or global configuration that can affect this inflector. It is deterministic.
|
422
|
-
|
423
|
-
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.
|
424
524
|
|
425
525
|
<a id="markdown-zeitwerkgeminflector" name="zeitwerkgeminflector"></a>
|
426
526
|
#### Zeitwerk::GemInflector
|
427
527
|
|
528
|
+
Each loader instantiated with `Zeitwerk::Loader.for_gem` has an inflector of this type by default.
|
529
|
+
|
428
530
|
This inflector is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`.
|
429
531
|
|
430
|
-
|
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.
|
431
533
|
|
432
534
|
<a id="markdown-custom-inflector" name="custom-inflector"></a>
|
433
535
|
#### Custom inflector
|
@@ -503,8 +605,26 @@ class MyGem::Inflector < Zeitwerk::GemInflector
|
|
503
605
|
end
|
504
606
|
```
|
505
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
|
+
|
506
626
|
<a id="markdown-the-on_load-callback" name="the-on_load-callback"></a>
|
507
|
-
|
627
|
+
#### The on_load callback
|
508
628
|
|
509
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.
|
510
630
|
|
@@ -522,26 +642,23 @@ With `on_load`, it is easy to schedule code at boot time that initializes `endpo
|
|
522
642
|
|
523
643
|
```ruby
|
524
644
|
# config/environments/development.rb
|
525
|
-
loader.on_load("SomeApiClient") do
|
526
|
-
|
645
|
+
loader.on_load("SomeApiClient") do |klass, _abspath|
|
646
|
+
klass.endpoint = "https://api.dev"
|
527
647
|
end
|
528
648
|
|
529
649
|
# config/environments/production.rb
|
530
|
-
loader.on_load("SomeApiClient") do
|
531
|
-
|
650
|
+
loader.on_load("SomeApiClient") do |klass, _abspath|
|
651
|
+
klass.endpoint = "https://api.prod"
|
532
652
|
end
|
533
653
|
```
|
534
654
|
|
535
|
-
|
655
|
+
Some uses cases:
|
536
656
|
|
537
|
-
* Doing something with
|
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.
|
538
658
|
* Delaying the execution of the block until the class is loaded for performance.
|
539
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.
|
540
|
-
* Etc.
|
541
660
|
|
542
|
-
|
543
|
-
|
544
|
-
`on_load` receives the name of the target class or module as a string. The given block is executed every time its corresponding file is loaded. That includes reloads.
|
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.
|
545
662
|
|
546
663
|
Multiple callbacks on the same target are supported, and they run in order of definition.
|
547
664
|
|
@@ -549,6 +666,68 @@ The block is executed once the loader has loaded the target. In particular, if t
|
|
549
666
|
|
550
667
|
Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
|
551
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
|
+
|
552
731
|
<a id="markdown-logging" name="logging"></a>
|
553
732
|
### Logging
|
554
733
|
|
@@ -717,6 +896,26 @@ Trip = Struct.new { ... } # NOT SUPPORTED
|
|
717
896
|
|
718
897
|
This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
|
719
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
|
+
|
720
919
|
<a id="markdown-reopening-third-party-namespaces" name="reopening-third-party-namespaces"></a>
|
721
920
|
### Reopening third-party namespaces
|
722
921
|
|
@@ -771,6 +970,11 @@ With that, when Zeitwerk scans the file system and reaches the gem directories `
|
|
771
970
|
<a id="markdown-debuggers" name="debuggers"></a>
|
772
971
|
### Debuggers
|
773
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
|
+
|
774
978
|
<a id="markdown-break" name="break"></a>
|
775
979
|
#### Break
|
776
980
|
|
@@ -789,7 +993,11 @@ Zeitwerk and [Byebug](https://github.com/deivid-rodriguez/byebug) are incompatib
|
|
789
993
|
<a id="markdown-supported-ruby-versions" name="supported-ruby-versions"></a>
|
790
994
|
## Supported Ruby versions
|
791
995
|
|
792
|
-
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.)
|
793
1001
|
|
794
1002
|
<a id="markdown-testing" name="testing"></a>
|
795
1003
|
## Testing
|
@@ -820,9 +1028,19 @@ and run `bin/test`.
|
|
820
1028
|
<a id="markdown-motivation" name="motivation"></a>
|
821
1029
|
## Motivation
|
822
1030
|
|
823
|
-
|
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
|
824
1042
|
|
825
|
-
|
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.
|
826
1044
|
|
827
1045
|
<a id="markdown-thanks" name="thanks"></a>
|
828
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
|
data/lib/zeitwerk/error.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Zeitwerk
|
2
4
|
# Centralizes the logic for the trace point used to detect the creation of
|
3
5
|
# explicit namespaces, needed to descend into matching subdirectories right
|
@@ -41,7 +43,7 @@ module Zeitwerk
|
|
41
43
|
|
42
44
|
# @private
|
43
45
|
# @sig (Zeitwerk::Loader) -> void
|
44
|
-
def
|
46
|
+
def unregister_loader(loader)
|
45
47
|
cpaths.delete_if { |_cpath, l| l == loader }
|
46
48
|
disable_tracer_if_unneeded
|
47
49
|
end
|
@@ -62,8 +64,14 @@ module Zeitwerk
|
|
62
64
|
# than accessing its name.
|
63
65
|
return if event.self.singleton_class?
|
64
66
|
|
65
|
-
#
|
66
|
-
#
|
67
|
+
# It might be tempting to return if name.nil?, to avoid the computation
|
68
|
+
# of a hash code and delete call. But Ruby does not trigger the :class
|
69
|
+
# event on Class.new or Module.new, so that would incur in an extra call
|
70
|
+
# for nothing.
|
71
|
+
#
|
72
|
+
# On the other hand, if we were called, cpaths is not empty. Otherwise
|
73
|
+
# the tracer is disabled. So we do need to go ahead with the hash code
|
74
|
+
# computation and delete call.
|
67
75
|
if loader = cpaths.delete(real_mod_name(event.self))
|
68
76
|
loader.on_namespace_loaded(event.self)
|
69
77
|
disable_tracer_if_unneeded
|