zeitwerk 2.4.2 → 2.5.0.beta4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +215 -68
- 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 +11 -8
- data/lib/zeitwerk/loader/config.rb +321 -0
- data/lib/zeitwerk/loader/helpers.rb +97 -0
- data/lib/zeitwerk/loader.rb +102 -416
- 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 +9 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4012f73de9387546f477e8254668d450c00310666bde922b90a6b2be8d4b9740
|
4
|
+
data.tar.gz: 7c403df9ea53494c2eb1a01b3fb81f70ce1a6488a420728f134c7c32ab4cb444
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f2222619050df7f4271a73b2abdc258d541249ed34a75738afb1661ba17460dc2a0c53690f33a85e33b5925e28a6e2d292fa9d12711722261ae517d6d38f7520
|
7
|
+
data.tar.gz: 77679e246700480f751e0bce8373d5fe648a78f41a2b8bd97cc0d9c0bdff52651bd64be59ad56809b2785539fd7439f3b9ad554d9ddb8f668fbca9b187d51a24
|
data/README.md
CHANGED
@@ -3,45 +3,56 @@
|
|
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)
|
17
22
|
- [Usage](#usage)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
- [Setup](#setup)
|
24
|
+
- [Generic](#generic)
|
25
|
+
- [for_gem](#for_gem)
|
26
|
+
- [Autoloading](#autoloading)
|
27
|
+
- [Eager loading](#eager-loading)
|
28
|
+
- [Reloading](#reloading)
|
29
|
+
- [Inflection](#inflection)
|
30
|
+
- [Zeitwerk::Inflector](#zeitwerkinflector)
|
31
|
+
- [Zeitwerk::GemInflector](#zeitwerkgeminflector)
|
32
|
+
- [Custom inflector](#custom-inflector)
|
33
|
+
- [Callbacks](#callbacks)
|
34
|
+
- [The on_setup callback](#the-on_setup-callback)
|
28
35
|
- [The on_load callback](#the-on_load-callback)
|
29
|
-
- [
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
- [
|
36
|
-
- [
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
36
|
+
- [The on_unload callback](#the-on_unload-callback)
|
37
|
+
- [Technical details](#technical-details)
|
38
|
+
- [Logging](#logging)
|
39
|
+
- [Loader tag](#loader-tag)
|
40
|
+
- [Ignoring parts of the project](#ignoring-parts-of-the-project)
|
41
|
+
- [Use case: Files that do not follow the conventions](#use-case-files-that-do-not-follow-the-conventions)
|
42
|
+
- [Use case: The adapter pattern](#use-case-the-adapter-pattern)
|
43
|
+
- [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
|
44
|
+
- [Edge cases](#edge-cases)
|
45
|
+
- [Reopening third-party namespaces](#reopening-third-party-namespaces)
|
46
|
+
- [Rules of thumb](#rules-of-thumb)
|
47
|
+
- [Debuggers](#debuggers)
|
48
|
+
- [Break](#break)
|
49
|
+
- [Byebug](#byebug)
|
41
50
|
- [Pronunciation](#pronunciation)
|
42
51
|
- [Supported Ruby versions](#supported-ruby-versions)
|
43
52
|
- [Testing](#testing)
|
44
53
|
- [Motivation](#motivation)
|
54
|
+
- [Kernel#require is brittle](#kernelrequire-is-brittle)
|
55
|
+
- [Rails autoloading was brittle](#rails-autoloading-was-brittle)
|
45
56
|
- [Thanks](#thanks)
|
46
57
|
- [License](#license)
|
47
58
|
|
@@ -117,6 +128,9 @@ Zeitwerk::Loader.eager_load_all
|
|
117
128
|
<a id="markdown-file-structure" name="file-structure"></a>
|
118
129
|
## File structure
|
119
130
|
|
131
|
+
<a id="markdown-the-idea-file-paths-match-constant-paths" name="the-idea-file-paths-match-constant-paths"></a>
|
132
|
+
### The idea: File paths match constant paths
|
133
|
+
|
120
134
|
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
135
|
|
122
136
|
```
|
@@ -126,25 +140,67 @@ lib/my_gem/bar_baz.rb -> MyGem::BarBaz
|
|
126
140
|
lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
|
127
141
|
```
|
128
142
|
|
129
|
-
|
143
|
+
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.
|
144
|
+
|
145
|
+
<a id="markdown-inner-simple-constants" name="inner-simple-constants"></a>
|
146
|
+
### Inner simple constants
|
147
|
+
|
148
|
+
While a simple constant like `HttpCrawler::MAX_RETRIES` can be defined in its own file:
|
130
149
|
|
131
150
|
```ruby
|
132
|
-
|
133
|
-
|
151
|
+
# http_crawler/max_retries.rb
|
152
|
+
HttpCrawler::MAX_RETRIES = 10
|
134
153
|
```
|
135
154
|
|
136
|
-
|
155
|
+
that is not required, you can also define it the regular way:
|
137
156
|
|
157
|
+
```ruby
|
158
|
+
# http_crawler.rb
|
159
|
+
class HttpCrawler
|
160
|
+
MAX_RETRIES = 10
|
161
|
+
end
|
138
162
|
```
|
139
|
-
|
140
|
-
|
163
|
+
|
164
|
+
The first example needs a custom [inflection](https://github.com/fxn/zeitwerk#inflection) rule:
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
loader.inflector.inflect("max_retries" => "MAX_RETRIES")
|
168
|
+
```
|
169
|
+
|
170
|
+
Otherwise, Zeitwerk would expect the file to define `MaxRetries`.
|
171
|
+
|
172
|
+
In the second example, no custom rule is needed.
|
173
|
+
|
174
|
+
<a id="markdown-root-directories-and-root-namespaces" name="root-directories-and-root-namespaces"></a>
|
175
|
+
### Root directories and root namespaces
|
176
|
+
|
177
|
+
Every directory configured with `push_dir` is called a _root directory_, and they represent _root namespaces_.
|
178
|
+
|
179
|
+
<a id="markdown-the-default-root-namespace-is-object" name="the-default-root-namespace-is-object"></a>
|
180
|
+
#### The default root namespace is `Object`
|
181
|
+
|
182
|
+
By default, the namespace associated to a root directory is the top-level one: `Object`.
|
183
|
+
|
184
|
+
For example, given
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
loader.push_dir("#{__dir__}/models")
|
188
|
+
loader.push_dir("#{__dir__}/serializers"))
|
141
189
|
```
|
142
190
|
|
143
|
-
|
191
|
+
these are the expected classes and modules being defined by these files:
|
144
192
|
|
145
|
-
|
193
|
+
```
|
194
|
+
models/user.rb -> User
|
195
|
+
serializers/user_serializer.rb -> UserSerializer
|
196
|
+
```
|
197
|
+
|
198
|
+
<a id="markdown-custom-root-namespaces" name="custom-root-namespaces"></a>
|
199
|
+
#### Custom root namespaces
|
200
|
+
|
201
|
+
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
202
|
|
147
|
-
|
203
|
+
For example, given:
|
148
204
|
|
149
205
|
```ruby
|
150
206
|
require "active_job"
|
@@ -152,9 +208,26 @@ require "active_job/queue_adapters"
|
|
152
208
|
loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
|
153
209
|
```
|
154
210
|
|
155
|
-
|
211
|
+
a file defining `ActiveJob::QueueAdapters::MyQueueAdapter` does not need the conventional parent directories, you can (and have to) store the file directly below `adapters`:
|
212
|
+
|
213
|
+
```
|
214
|
+
adapters/my_queue_adapter.rb -> ActiveJob::QueueAdapters::MyQueueAdapter
|
215
|
+
```
|
216
|
+
|
217
|
+
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.
|
218
|
+
|
219
|
+
<a id="markdown-nested-root-directories" name="nested-root-directories"></a>
|
220
|
+
#### Nested root directories
|
156
221
|
|
157
|
-
|
222
|
+
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.
|
223
|
+
|
224
|
+
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:
|
225
|
+
|
226
|
+
```
|
227
|
+
app/models/concerns/geolocatable.rb
|
228
|
+
```
|
229
|
+
|
230
|
+
should define `Geolocatable`, not `Concerns::Geolocatable`.
|
158
231
|
|
159
232
|
<a id="markdown-implicit-namespaces" name="implicit-namespaces"></a>
|
160
233
|
### Implicit namespaces
|
@@ -216,19 +289,6 @@ To illustrate usage of glob patterns, if `actions` in the example above is part
|
|
216
289
|
loader.collapse("#{__dir__}/*/actions")
|
217
290
|
```
|
218
291
|
|
219
|
-
<a id="markdown-nested-root-directories" name="nested-root-directories"></a>
|
220
|
-
### Nested root directories
|
221
|
-
|
222
|
-
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.
|
223
|
-
|
224
|
-
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:
|
225
|
-
|
226
|
-
```
|
227
|
-
app/models/concerns/geolocatable.rb
|
228
|
-
```
|
229
|
-
|
230
|
-
should define `Geolocatable`, not `Concerns::Geolocatable`.
|
231
|
-
|
232
292
|
<a id="markdown-usage" name="usage"></a>
|
233
293
|
## Usage
|
234
294
|
|
@@ -387,11 +447,13 @@ On reloading, client code has to update anything that would otherwise be storing
|
|
387
447
|
<a id="markdown-inflection" name="inflection"></a>
|
388
448
|
### Inflection
|
389
449
|
|
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.
|
450
|
+
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
451
|
|
392
452
|
<a id="markdown-zeitwerkinflector" name="zeitwerkinflector"></a>
|
393
453
|
#### Zeitwerk::Inflector
|
394
454
|
|
455
|
+
Each loader instantiated with `Zeitwerk::Loader.new` has an inflector of this type by default.
|
456
|
+
|
395
457
|
This is a very basic inflector that converts snake case to camel case:
|
396
458
|
|
397
459
|
```
|
@@ -418,16 +480,16 @@ loader.inflector.inflect "mysql_adapter" => "MySQLAdapter"
|
|
418
480
|
|
419
481
|
Overrides need to be configured before calling `setup`.
|
420
482
|
|
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.
|
483
|
+
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
484
|
|
425
485
|
<a id="markdown-zeitwerkgeminflector" name="zeitwerkgeminflector"></a>
|
426
486
|
#### Zeitwerk::GemInflector
|
427
487
|
|
488
|
+
Each loader instantiated with `Zeitwerk::Loader.for_gem` has an inflector of this type by default.
|
489
|
+
|
428
490
|
This inflector is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`.
|
429
491
|
|
430
|
-
|
492
|
+
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
493
|
|
432
494
|
<a id="markdown-custom-inflector" name="custom-inflector"></a>
|
433
495
|
#### Custom inflector
|
@@ -503,8 +565,26 @@ class MyGem::Inflector < Zeitwerk::GemInflector
|
|
503
565
|
end
|
504
566
|
```
|
505
567
|
|
568
|
+
<a id="markdown-callbacks" name="callbacks"></a>
|
569
|
+
### Callbacks
|
570
|
+
|
571
|
+
<a id="markdown-the-on_setup-callback" name="the-on_setup-callback"></a>
|
572
|
+
#### The on_setup callback
|
573
|
+
|
574
|
+
The `on_setup` callback is fired on setup and on each reload:
|
575
|
+
|
576
|
+
```ruby
|
577
|
+
loader.on_setup do
|
578
|
+
# Ready to autoload here.
|
579
|
+
end
|
580
|
+
```
|
581
|
+
|
582
|
+
Multiple `on_setup` callbacks are supported, and they run in order of definition.
|
583
|
+
|
584
|
+
If `setup` was already executed, the callback is fired immediately.
|
585
|
+
|
506
586
|
<a id="markdown-the-on_load-callback" name="the-on_load-callback"></a>
|
507
|
-
|
587
|
+
#### The on_load callback
|
508
588
|
|
509
589
|
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
590
|
|
@@ -522,26 +602,23 @@ With `on_load`, it is easy to schedule code at boot time that initializes `endpo
|
|
522
602
|
|
523
603
|
```ruby
|
524
604
|
# config/environments/development.rb
|
525
|
-
loader.on_load("SomeApiClient") do
|
526
|
-
|
605
|
+
loader.on_load("SomeApiClient") do |klass, _abspath|
|
606
|
+
klass.endpoint = "https://api.dev"
|
527
607
|
end
|
528
608
|
|
529
609
|
# config/environments/production.rb
|
530
|
-
loader.on_load("SomeApiClient") do
|
531
|
-
|
610
|
+
loader.on_load("SomeApiClient") do |klass, _abspath|
|
611
|
+
klass.endpoint = "https://api.prod"
|
532
612
|
end
|
533
613
|
```
|
534
614
|
|
535
|
-
|
615
|
+
Some uses cases:
|
536
616
|
|
537
|
-
* Doing something with
|
617
|
+
* 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
618
|
* Delaying the execution of the block until the class is loaded for performance.
|
539
619
|
* 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
620
|
|
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.
|
621
|
+
`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
622
|
|
546
623
|
Multiple callbacks on the same target are supported, and they run in order of definition.
|
547
624
|
|
@@ -549,6 +626,66 @@ The block is executed once the loader has loaded the target. In particular, if t
|
|
549
626
|
|
550
627
|
Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
|
551
628
|
|
629
|
+
It is also possible to be called when any constant managed by the loader is loaded:
|
630
|
+
|
631
|
+
```ruby
|
632
|
+
loader.on_load do |cpath, value, abspath|
|
633
|
+
# ...
|
634
|
+
end
|
635
|
+
```
|
636
|
+
|
637
|
+
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.
|
638
|
+
|
639
|
+
Multiple callbacks like these are supported, and they run in order of definition.
|
640
|
+
|
641
|
+
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.
|
642
|
+
|
643
|
+
If both types of callbacks are defined, the specific ones run first.
|
644
|
+
|
645
|
+
<a id="markdown-the-on_unload-callback" name="the-on_unload-callback"></a>
|
646
|
+
#### The on_unload callback
|
647
|
+
|
648
|
+
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.
|
649
|
+
|
650
|
+
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:
|
651
|
+
|
652
|
+
```ruby
|
653
|
+
loader.on_unload("Country") do |klass, _abspath|
|
654
|
+
klass.clear_cache
|
655
|
+
end
|
656
|
+
```
|
657
|
+
|
658
|
+
`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.
|
659
|
+
|
660
|
+
`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.
|
661
|
+
|
662
|
+
Multiple callbacks on the same target are supported, and they run in order of definition.
|
663
|
+
|
664
|
+
Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
|
665
|
+
|
666
|
+
It is also possible to be called when any constant managed by the loader is unloaded:
|
667
|
+
|
668
|
+
```ruby
|
669
|
+
loader.on_unload do |cpath, value, abspath|
|
670
|
+
# ...
|
671
|
+
end
|
672
|
+
```
|
673
|
+
|
674
|
+
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.
|
675
|
+
|
676
|
+
Multiple callbacks like these are supported, and they run in order of definition.
|
677
|
+
|
678
|
+
If both types of callbacks are defined, the specific ones run first.
|
679
|
+
|
680
|
+
<a id="markdown-technical-details" name="technical-details"></a>
|
681
|
+
#### Technical details
|
682
|
+
|
683
|
+
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?
|
684
|
+
|
685
|
+
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.
|
686
|
+
|
687
|
+
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.
|
688
|
+
|
552
689
|
<a id="markdown-logging" name="logging"></a>
|
553
690
|
### Logging
|
554
691
|
|
@@ -820,9 +957,19 @@ and run `bin/test`.
|
|
820
957
|
<a id="markdown-motivation" name="motivation"></a>
|
821
958
|
## Motivation
|
822
959
|
|
823
|
-
|
960
|
+
<a id="markdown-kernelrequire-is-brittle" name="kernelrequire-is-brittle"></a>
|
961
|
+
### Kernel#require is brittle
|
962
|
+
|
963
|
+
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.
|
964
|
+
|
965
|
+
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.
|
966
|
+
|
967
|
+
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.
|
968
|
+
|
969
|
+
<a id="markdown-rails-autoloading-was-brittle" name="rails-autoloading-was-brittle"></a>
|
970
|
+
### Rails autoloading was brittle
|
824
971
|
|
825
|
-
|
972
|
+
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
973
|
|
827
974
|
<a id="markdown-thanks" name="thanks"></a>
|
828
975
|
## 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
|
data/lib/zeitwerk/kernel.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
module Kernel
|
4
4
|
module_function
|
5
5
|
|
6
|
-
# We are going to decorate
|
6
|
+
# We are going to decorate Kernel#require with two goals.
|
7
7
|
#
|
8
8
|
# First, by intercepting Kernel#require calls, we are able to autovivify
|
9
9
|
# modules on required directories, and also do internal housekeeping when
|
@@ -12,7 +12,8 @@ module Kernel
|
|
12
12
|
# On the other hand, if you publish a new version of a gem that is now managed
|
13
13
|
# by Zeitwerk, client code can reference directly your classes and modules and
|
14
14
|
# should not require anything. But if someone has legacy require calls around,
|
15
|
-
# they will work as expected, and in a compatible way.
|
15
|
+
# they will work as expected, and in a compatible way. This feature is by now
|
16
|
+
# EXPERIMENTAL and UNDOCUMENTED.
|
16
17
|
#
|
17
18
|
# We cannot decorate with prepend + super because Kernel has already been
|
18
19
|
# included in Object, and changes in ancestors don't get propagated into
|
@@ -33,9 +34,9 @@ module Kernel
|
|
33
34
|
else
|
34
35
|
zeitwerk_original_require(path).tap do |required|
|
35
36
|
if required
|
36
|
-
|
37
|
-
if loader = Zeitwerk::Registry.loader_for(
|
38
|
-
loader.on_file_autoloaded(
|
37
|
+
abspath = $LOADED_FEATURES.last
|
38
|
+
if loader = Zeitwerk::Registry.loader_for(abspath)
|
39
|
+
loader.on_file_autoloaded(abspath)
|
39
40
|
end
|
40
41
|
end
|
41
42
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Zeitwerk::Loader::Callbacks
|
2
4
|
include Zeitwerk::RealModName
|
3
5
|
|
@@ -18,7 +20,7 @@ module Zeitwerk::Loader::Callbacks
|
|
18
20
|
raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath}, but didn't", cref.last)
|
19
21
|
end
|
20
22
|
|
21
|
-
run_on_load_callbacks(cpath)
|
23
|
+
run_on_load_callbacks(cpath, cget(*cref), file) unless on_load_callbacks.empty?
|
22
24
|
end
|
23
25
|
|
24
26
|
# Invoked from our decorated Kernel#require when a managed directory is
|
@@ -54,7 +56,7 @@ module Zeitwerk::Loader::Callbacks
|
|
54
56
|
|
55
57
|
on_namespace_loaded(autovivified_module)
|
56
58
|
|
57
|
-
run_on_load_callbacks(cpath)
|
59
|
+
run_on_load_callbacks(cpath, autovivified_module, dir) unless on_load_callbacks.empty?
|
58
60
|
end
|
59
61
|
end
|
60
62
|
end
|
@@ -75,12 +77,13 @@ module Zeitwerk::Loader::Callbacks
|
|
75
77
|
|
76
78
|
private
|
77
79
|
|
78
|
-
# @sig (String) -> void
|
79
|
-
def run_on_load_callbacks(cpath)
|
80
|
-
#
|
81
|
-
return if on_load_callbacks.empty?
|
82
|
-
|
80
|
+
# @sig (String, Object) -> void
|
81
|
+
def run_on_load_callbacks(cpath, value, abspath)
|
82
|
+
# Order matters. If present, run the most specific one.
|
83
83
|
callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)
|
84
|
-
callbacks.
|
84
|
+
callbacks&.each { |c| c.call(value, abspath) }
|
85
|
+
|
86
|
+
callbacks = on_load_callbacks[:ANY]
|
87
|
+
callbacks&.each { |c| c.call(cpath, value, abspath) }
|
85
88
|
end
|
86
89
|
end
|