zeitwerk 2.6.18 → 2.8.1
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 +152 -115
- data/lib/zeitwerk/{kernel.rb → core_ext/kernel.rb} +4 -4
- data/lib/zeitwerk/core_ext/module.rb +20 -0
- data/lib/zeitwerk/cref/map.rb +159 -0
- data/lib/zeitwerk/cref.rb +29 -52
- data/lib/zeitwerk/error.rb +14 -1
- data/lib/zeitwerk/gem_inflector.rb +5 -5
- data/lib/zeitwerk/gem_loader.rb +10 -10
- data/lib/zeitwerk/inflector.rb +11 -11
- data/lib/zeitwerk/internal.rb +1 -0
- data/lib/zeitwerk/loader/callbacks.rb +21 -24
- data/lib/zeitwerk/loader/config.rb +109 -70
- data/lib/zeitwerk/loader/eager_load.rb +50 -56
- data/lib/zeitwerk/loader/file_system.rb +212 -0
- data/lib/zeitwerk/loader/helpers.rb +4 -106
- data/lib/zeitwerk/loader.rb +314 -239
- data/lib/zeitwerk/null_inflector.rb +1 -0
- data/lib/zeitwerk/real_mod_name.rb +9 -12
- data/lib/zeitwerk/registry/autoloads.rb +38 -0
- data/lib/zeitwerk/registry/explicit_namespaces.rb +61 -0
- data/lib/zeitwerk/registry/inceptions.rb +31 -0
- data/lib/zeitwerk/registry/loaders.rb +33 -0
- data/lib/zeitwerk/registry.rb +41 -93
- data/lib/zeitwerk/version.rb +2 -1
- data/lib/zeitwerk.rb +15 -14
- metadata +13 -10
- data/lib/zeitwerk/explicit_namespace.rb +0 -93
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f6f3971296735d2ad5ae2d2621b5f26d6f480e7b69fde7e6eb21679a2b0c795c
|
|
4
|
+
data.tar.gz: 41ce43ff32412f63d21fb5bd9f45dc070e48e96f03be1e49eb2a5c6c5ef107ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b543e2a0db16c3dcdf8318faeaae7e1a72b0001b6116fa0081fa991266fbcc75da8ebdecaf9cf17568d9b169688810f411fb5d578a6ce7f5fad60a5fea37380
|
|
7
|
+
data.tar.gz: 021343c474a12a015071b20eaff22c53fdf9d0a49e606ee460be809fd731be065e72573ffb082bfab19b14f432000b309cc3e8e44e75669b97d91fa5b275519a
|
data/README.md
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
- [Nested root directories](#nested-root-directories)
|
|
20
20
|
- [Implicit namespaces](#implicit-namespaces)
|
|
21
21
|
- [Explicit namespaces](#explicit-namespaces)
|
|
22
|
+
- [Explicit namespaces defined in ordinary files](#explicit-namespaces-defined-in-ordinary-files)
|
|
23
|
+
- [Explicit namespaces defined in nsfiles](#explicit-namespaces-defined-in-nsfiles)
|
|
22
24
|
- [Collapsing directories](#collapsing-directories)
|
|
23
25
|
- [Testing compliance](#testing-compliance)
|
|
24
26
|
- [Usage](#usage)
|
|
@@ -54,16 +56,15 @@
|
|
|
54
56
|
- [Use case: The adapter pattern](#use-case-the-adapter-pattern)
|
|
55
57
|
- [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
|
|
56
58
|
- [Shadowed files](#shadowed-files)
|
|
57
|
-
- [Edge cases](#edge-cases)
|
|
58
59
|
- [Beware of circular dependencies](#beware-of-circular-dependencies)
|
|
59
60
|
- [Reopening third-party namespaces](#reopening-third-party-namespaces)
|
|
60
61
|
- [Introspection](#introspection)
|
|
61
62
|
- [`Zeitwerk::Loader#dirs`](#zeitwerkloaderdirs)
|
|
63
|
+
- [Autoloaded Constants](#autoloaded-constants)
|
|
62
64
|
- [`Zeitwerk::Loader#cpath_expected_at`](#zeitwerkloadercpath_expected_at)
|
|
63
65
|
- [`Zeitwerk::Loader#all_expected_cpaths`](#zeitwerkloaderall_expected_cpaths)
|
|
64
66
|
- [Encodings](#encodings)
|
|
65
67
|
- [Rules of thumb](#rules-of-thumb)
|
|
66
|
-
- [Debuggers](#debuggers)
|
|
67
68
|
- [Pronunciation](#pronunciation)
|
|
68
69
|
- [Supported Ruby versions](#supported-ruby-versions)
|
|
69
70
|
- [Testing](#testing)
|
|
@@ -99,7 +100,7 @@ Main interface for gems:
|
|
|
99
100
|
```ruby
|
|
100
101
|
# lib/my_gem.rb (main file)
|
|
101
102
|
|
|
102
|
-
require
|
|
103
|
+
require 'zeitwerk'
|
|
103
104
|
loader = Zeitwerk::Loader.for_gem
|
|
104
105
|
loader.setup # ready!
|
|
105
106
|
|
|
@@ -182,7 +183,7 @@ end
|
|
|
182
183
|
The first example needs a custom [inflection](#inflection) rule:
|
|
183
184
|
|
|
184
185
|
```ruby
|
|
185
|
-
loader.inflector.inflect(
|
|
186
|
+
loader.inflector.inflect('max_retries' => 'MAX_RETRIES')
|
|
186
187
|
```
|
|
187
188
|
|
|
188
189
|
Otherwise, Zeitwerk would expect the file to define `MaxRetries`.
|
|
@@ -203,7 +204,7 @@ For example, given
|
|
|
203
204
|
|
|
204
205
|
```ruby
|
|
205
206
|
loader.push_dir("#{__dir__}/models")
|
|
206
|
-
loader.push_dir("#{__dir__}/serializers")
|
|
207
|
+
loader.push_dir("#{__dir__}/serializers")
|
|
207
208
|
```
|
|
208
209
|
|
|
209
210
|
these are the expected classes and modules being defined by these files:
|
|
@@ -221,8 +222,8 @@ Although `Object` is the most common root namespace, you have the flexibility to
|
|
|
221
222
|
For example, given:
|
|
222
223
|
|
|
223
224
|
```ruby
|
|
224
|
-
require
|
|
225
|
-
require
|
|
225
|
+
require 'active_job'
|
|
226
|
+
require 'active_job/queue_adapters'
|
|
226
227
|
loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
|
|
227
228
|
```
|
|
228
229
|
|
|
@@ -265,16 +266,23 @@ To trigger this behavior, the directory must contain non-ignored Ruby files with
|
|
|
265
266
|
<a id="markdown-explicit-namespaces" name="explicit-namespaces"></a>
|
|
266
267
|
### Explicit namespaces
|
|
267
268
|
|
|
268
|
-
Classes and modules that act as namespaces can also be explicitly defined
|
|
269
|
+
Classes and modules that act as namespaces can also be explicitly defined in a file. This can be done with ordinary files named after the corresponding constant path, or with special namespace files, or _nsfiles_ for short.
|
|
270
|
+
|
|
271
|
+
<a id="markdown-explicit-namespaces-defined-in-ordinary-files" name="explicit-namespaces-defined-in-ordinary-files"></a>
|
|
272
|
+
#### Explicit namespaces defined in ordinary files
|
|
273
|
+
|
|
274
|
+
Let's consider:
|
|
269
275
|
|
|
270
276
|
```
|
|
271
277
|
app/models/hotel.rb -> Hotel
|
|
272
278
|
app/models/hotel/pricing.rb -> Hotel::Pricing
|
|
273
279
|
```
|
|
274
280
|
|
|
275
|
-
|
|
281
|
+
Since there is a file `app/models/hotel.rb` and also a directory `app/models/hotel`, Zeitwerk realizes `Hotel` is a namespace that is defined in `app/models/hotel.rb`.
|
|
276
282
|
|
|
277
|
-
|
|
283
|
+
In order to realize this, the directory or directories conforming the namespace do not need to be next to the file, as in the example, they could be in some other root directory.
|
|
284
|
+
|
|
285
|
+
The classes and modules from an explicit namespace are already available in the body of the class or module that defines it:
|
|
278
286
|
|
|
279
287
|
```ruby
|
|
280
288
|
class Hotel < ApplicationRecord
|
|
@@ -283,7 +291,50 @@ class Hotel < ApplicationRecord
|
|
|
283
291
|
end
|
|
284
292
|
```
|
|
285
293
|
|
|
286
|
-
|
|
294
|
+
When autoloaded, Zeitwerk verifies the expected constant (`Hotel` in the example) stores a class or module object. If it doesn't, `Zeitwerk::Error` is raised.
|
|
295
|
+
|
|
296
|
+
<a id="markdown-explicit-namespaces-defined-in-nsfiles" name="explicit-namespaces-defined-in-nsfiles"></a>
|
|
297
|
+
#### Explicit namespaces defined in nsfiles
|
|
298
|
+
|
|
299
|
+
If the loader has an nsfile configured (defaults to `nil`):
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
loader.nsfile = 'ns.rb' # must be set before setup
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
you can alternatively define the explicit namespace inside its directory:
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
my_component/ns.rb -> MyComponent
|
|
309
|
+
my_component/widget.rb -> MyComponent::Widget
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
This may be handy for self-contained units for which a `my_component.rb` file in the parent directory would feel unnatural.
|
|
313
|
+
|
|
314
|
+
A loader's nsfile has to be a non-hidden basename with a `.rb` extension, as in the example above. Nsfiles are not inflected, so as long as those conditions hold, they may contain leading underscores, hyphens, etc.
|
|
315
|
+
|
|
316
|
+
Collapsed directories work as expected. For example, if we assume that `src` is collapsed, and that `assets` and `tests` are ignored, you could have the code organized this way:
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
my_component/src/ns.rb -> MyComponent
|
|
320
|
+
my_component/src/widget.rb -> MyComponent::Widget
|
|
321
|
+
my_component/assets/widget.js
|
|
322
|
+
my_component/tests/test_widget.rb
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Loaders with an nsfile configured also support explicit namespaces defined in ordinary files. The conventions are not exclusive project-wide. Some parts may be component-oriented, while in other parts ordinary files may feel more natural. That works.
|
|
326
|
+
|
|
327
|
+
However, attempting to define the same namespace using an ordinary file and an nsfile is an error condition that raises `Zeitwerk::ConflictingNamespaceDefinitionError`.
|
|
328
|
+
|
|
329
|
+
Nsfiles in root directories raise `Zeitwerk::ConflictingNamespaceDefinitionError` too, since the namespace in a root directory is externally defined.
|
|
330
|
+
|
|
331
|
+
Non-ignored files whose basename is equal to the nsfile are always considered to be nsfiles. You cannot opt out. Therefore, if we have:
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
loader.nsfile = 'index.rb'
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
there is no way `foo/index.rb` can define `Foo::Index` in any part of the project, it must define `Foo`.
|
|
287
338
|
|
|
288
339
|
<a id="markdown-collapsing-directories" name="collapsing-directories"></a>
|
|
289
340
|
### Collapsing directories
|
|
@@ -371,9 +422,9 @@ Conceptually, `for_gem` translates to:
|
|
|
371
422
|
```ruby
|
|
372
423
|
# lib/my_gem.rb
|
|
373
424
|
|
|
374
|
-
require
|
|
425
|
+
require 'zeitwerk'
|
|
375
426
|
loader = Zeitwerk::Loader.new
|
|
376
|
-
loader.tag = File.basename(__FILE__,
|
|
427
|
+
loader.tag = File.basename(__FILE__, '.rb')
|
|
377
428
|
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
|
378
429
|
loader.push_dir(File.dirname(__FILE__))
|
|
379
430
|
```
|
|
@@ -383,7 +434,7 @@ If the main module references project constants at the top-level, Zeitwerk has t
|
|
|
383
434
|
```ruby
|
|
384
435
|
# lib/my_gem.rb (main file)
|
|
385
436
|
|
|
386
|
-
require
|
|
437
|
+
require 'zeitwerk'
|
|
387
438
|
loader = Zeitwerk::Loader.for_gem
|
|
388
439
|
loader.setup
|
|
389
440
|
|
|
@@ -429,7 +480,7 @@ Let's suppose you are writing a gem to extend `Net::HTTP` with some niche featur
|
|
|
429
480
|
The top-level file mentioned in the last point is optional. In particular, from
|
|
430
481
|
|
|
431
482
|
```ruby
|
|
432
|
-
gem
|
|
483
|
+
gem 'net-http-niche_feature'
|
|
433
484
|
```
|
|
434
485
|
|
|
435
486
|
if the hyphenated file does not exist, Bundler notes the conventional hyphenated pattern and issues a `require` for `net/http/niche_feature`.
|
|
@@ -442,13 +493,13 @@ The structure of the gem would be like this:
|
|
|
442
493
|
# lib/net-http-niche_feature.rb (optional)
|
|
443
494
|
|
|
444
495
|
# For technical reasons, this cannot be require_relative.
|
|
445
|
-
require
|
|
496
|
+
require 'net/http/niche_feature'
|
|
446
497
|
|
|
447
498
|
|
|
448
499
|
# lib/net/http/niche_feature.rb
|
|
449
500
|
|
|
450
|
-
require
|
|
451
|
-
require
|
|
501
|
+
require 'net/http'
|
|
502
|
+
require 'zeitwerk'
|
|
452
503
|
|
|
453
504
|
loader = Zeitwerk::Loader.for_gem_extension(Net::HTTP)
|
|
454
505
|
loader.setup
|
|
@@ -463,7 +514,7 @@ end
|
|
|
463
514
|
# lib/net/http/niche_feature/version.rb
|
|
464
515
|
|
|
465
516
|
module Net::HTTP::NicheFeature
|
|
466
|
-
VERSION =
|
|
517
|
+
VERSION = '1.0.0'
|
|
467
518
|
end
|
|
468
519
|
```
|
|
469
520
|
|
|
@@ -485,7 +536,7 @@ Let's revisit the example above:
|
|
|
485
536
|
```ruby
|
|
486
537
|
# lib/my_gem.rb (main file)
|
|
487
538
|
|
|
488
|
-
require
|
|
539
|
+
require 'zeitwerk'
|
|
489
540
|
loader = Zeitwerk::Loader.for_gem
|
|
490
541
|
loader.setup
|
|
491
542
|
|
|
@@ -494,7 +545,7 @@ module MyGem
|
|
|
494
545
|
end
|
|
495
546
|
```
|
|
496
547
|
|
|
497
|
-
That works, and there is no `require
|
|
548
|
+
That works, and there is no `require 'my_gem/my_logger'`. When `(*)` is reached, Zeitwerk seamlessly autoloads `MyGem::MyLogger`.
|
|
498
549
|
|
|
499
550
|
If autoloading a file does not define the expected class or module, Zeitwerk raises `Zeitwerk::NameError`, which is a subclass of `NameError`.
|
|
500
551
|
|
|
@@ -681,7 +732,7 @@ In order to reload safely, no other thread can be autoloading or reloading concu
|
|
|
681
732
|
For example, a web framework that serves each request in its own thread and has reloading enabled could create a read-write lock on boot like this:
|
|
682
733
|
|
|
683
734
|
```ruby
|
|
684
|
-
require
|
|
735
|
+
require 'concurrent/atomic/read_write_lock'
|
|
685
736
|
|
|
686
737
|
MyFramework::RELOAD_RW_LOCK = Concurrent::ReadWriteLock.new
|
|
687
738
|
```
|
|
@@ -726,22 +777,22 @@ The camelize logic can be overridden easily for individual basenames:
|
|
|
726
777
|
|
|
727
778
|
```ruby
|
|
728
779
|
loader.inflector.inflect(
|
|
729
|
-
|
|
730
|
-
|
|
780
|
+
'html_parser' => 'HTMLParser',
|
|
781
|
+
'mysql_adapter' => 'MySQLAdapter'
|
|
731
782
|
)
|
|
732
783
|
```
|
|
733
784
|
|
|
734
785
|
The `inflect` method can be invoked several times if you prefer this other style:
|
|
735
786
|
|
|
736
787
|
```ruby
|
|
737
|
-
loader.inflector.inflect
|
|
738
|
-
loader.inflector.inflect
|
|
788
|
+
loader.inflector.inflect 'html_parser' => 'HTMLParser'
|
|
789
|
+
loader.inflector.inflect 'mysql_adapter' => 'MySQLAdapter'
|
|
739
790
|
```
|
|
740
791
|
|
|
741
792
|
Overrides have to match exactly directory or file (without extension) _basenames_. For example, if you configure
|
|
742
793
|
|
|
743
794
|
```ruby
|
|
744
|
-
loader.inflector.inflect(
|
|
795
|
+
loader.inflector.inflect('xml' => 'XML')
|
|
745
796
|
```
|
|
746
797
|
|
|
747
798
|
then the following constants are expected:
|
|
@@ -756,8 +807,8 @@ As you see, any directory whose basename is exactly `xml`, and any file whose ba
|
|
|
756
807
|
|
|
757
808
|
```ruby
|
|
758
809
|
loader.inflector.inflect(
|
|
759
|
-
|
|
760
|
-
|
|
810
|
+
'xml' => 'XML',
|
|
811
|
+
'xml_parser' => 'XMLParser'
|
|
761
812
|
)
|
|
762
813
|
```
|
|
763
814
|
|
|
@@ -812,7 +863,7 @@ The inflectors that ship with Zeitwerk are deterministic and simple. But you can
|
|
|
812
863
|
class MyInflector < Zeitwerk::Inflector
|
|
813
864
|
def camelize(basename, abspath)
|
|
814
865
|
if basename =~ /\Ahtml_(.*)/
|
|
815
|
-
|
|
866
|
+
'HTML' + super($1, abspath)
|
|
816
867
|
else
|
|
817
868
|
super
|
|
818
869
|
end
|
|
@@ -843,8 +894,8 @@ module MyGem
|
|
|
843
894
|
end
|
|
844
895
|
|
|
845
896
|
# lib/my_gem.rb
|
|
846
|
-
require
|
|
847
|
-
require_relative
|
|
897
|
+
require 'zeitwerk'
|
|
898
|
+
require_relative 'my_gem/inflector'
|
|
848
899
|
|
|
849
900
|
loader = Zeitwerk::Loader.for_gem
|
|
850
901
|
loader.inflector = MyGem::Inflector.new(__FILE__)
|
|
@@ -912,13 +963,13 @@ With `on_load`, it is easy to schedule code at boot time that initializes `endpo
|
|
|
912
963
|
|
|
913
964
|
```ruby
|
|
914
965
|
# config/environments/development.rb
|
|
915
|
-
loader.on_load(
|
|
916
|
-
klass.endpoint =
|
|
966
|
+
loader.on_load('SomeApiClient') do |klass, _abspath|
|
|
967
|
+
klass.endpoint = 'https://api.dev'
|
|
917
968
|
end
|
|
918
969
|
|
|
919
970
|
# config/environments/production.rb
|
|
920
|
-
loader.on_load(
|
|
921
|
-
klass.endpoint =
|
|
971
|
+
loader.on_load('SomeApiClient') do |klass, _abspath|
|
|
972
|
+
klass.endpoint = 'https://api.prod'
|
|
922
973
|
end
|
|
923
974
|
```
|
|
924
975
|
|
|
@@ -962,7 +1013,7 @@ When reloading is enabled, you may occasionally need to execute something before
|
|
|
962
1013
|
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:
|
|
963
1014
|
|
|
964
1015
|
```ruby
|
|
965
|
-
loader.on_unload(
|
|
1016
|
+
loader.on_unload('Country') do |klass, _abspath|
|
|
966
1017
|
klass.clear_cache
|
|
967
1018
|
end
|
|
968
1019
|
```
|
|
@@ -1047,7 +1098,7 @@ Zeitwerk@9fa54b: autoload set for User, to be loaded from ...
|
|
|
1047
1098
|
By default, a random tag like the one above is assigned, but you can change it:
|
|
1048
1099
|
|
|
1049
1100
|
```
|
|
1050
|
-
loader.tag =
|
|
1101
|
+
loader.tag = 'grep_me'
|
|
1051
1102
|
```
|
|
1052
1103
|
|
|
1053
1104
|
The tag of a loader returned by `for_gem` is the basename of the root file without extension:
|
|
@@ -1103,7 +1154,7 @@ loader.setup
|
|
|
1103
1154
|
Now, that file has to be loaded manually with `require` or `require_relative`:
|
|
1104
1155
|
|
|
1105
1156
|
```ruby
|
|
1106
|
-
require_relative
|
|
1157
|
+
require_relative 'my_gem/core_ext/kernel'
|
|
1107
1158
|
```
|
|
1108
1159
|
|
|
1109
1160
|
and you can do that anytime, before configuring the loader, or after configuring the loader, does not matter.
|
|
@@ -1117,7 +1168,7 @@ Let's imagine your project talks to databases, supports several, and has adapter
|
|
|
1117
1168
|
|
|
1118
1169
|
```ruby
|
|
1119
1170
|
# my_gem/db_adapters/postgresql.rb
|
|
1120
|
-
require
|
|
1171
|
+
require 'pg'
|
|
1121
1172
|
```
|
|
1122
1173
|
|
|
1123
1174
|
but you don't want your users to install them all, only the one they are going to use.
|
|
@@ -1155,7 +1206,7 @@ loader.setup
|
|
|
1155
1206
|
In Ruby, if you have several files called `foo.rb` in different directories of `$LOAD_PATH` and execute
|
|
1156
1207
|
|
|
1157
1208
|
```ruby
|
|
1158
|
-
require
|
|
1209
|
+
require 'foo'
|
|
1159
1210
|
```
|
|
1160
1211
|
|
|
1161
1212
|
the first one found gets loaded, and the rest are ignored.
|
|
@@ -1178,36 +1229,6 @@ file #{file} is ignored because #{constant_path} is already defined
|
|
|
1178
1229
|
|
|
1179
1230
|
Shadowing only applies to Ruby files, namespace definition can be spread over multiple directories. And you can also reopen third-party namespaces if done [orderly](#reopening-third-party-namespaces).
|
|
1180
1231
|
|
|
1181
|
-
<a id="markdown-edge-cases" name="edge-cases"></a>
|
|
1182
|
-
### Edge cases
|
|
1183
|
-
|
|
1184
|
-
[Explicit namespaces](#explicit-namespaces) like `Trip` here:
|
|
1185
|
-
|
|
1186
|
-
```ruby
|
|
1187
|
-
# trip.rb
|
|
1188
|
-
class Trip
|
|
1189
|
-
include Geolocation
|
|
1190
|
-
end
|
|
1191
|
-
|
|
1192
|
-
# trip/geolocation.rb
|
|
1193
|
-
module Trip::Geolocation
|
|
1194
|
-
...
|
|
1195
|
-
end
|
|
1196
|
-
```
|
|
1197
|
-
|
|
1198
|
-
have to be defined with the `class`/`module` keywords, as in the example above.
|
|
1199
|
-
|
|
1200
|
-
For technical reasons, raw constant assignment is not supported:
|
|
1201
|
-
|
|
1202
|
-
```ruby
|
|
1203
|
-
# trip.rb
|
|
1204
|
-
Trip = Class { ...} # NOT SUPPORTED
|
|
1205
|
-
Trip = Struct.new { ... } # NOT SUPPORTED
|
|
1206
|
-
Trip = Data.define { ... } # NOT SUPPORTED
|
|
1207
|
-
```
|
|
1208
|
-
|
|
1209
|
-
This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
|
|
1210
|
-
|
|
1211
1232
|
<a id="markdown-beware-of-circular-dependencies" name="beware-of-circular-dependencies"></a>
|
|
1212
1233
|
### Beware of circular dependencies
|
|
1213
1234
|
|
|
@@ -1254,10 +1275,10 @@ In order to do so, you need to make sure those modules are loaded before calling
|
|
|
1254
1275
|
|
|
1255
1276
|
```ruby
|
|
1256
1277
|
# Ensure these namespaces are reopened, not defined.
|
|
1257
|
-
require
|
|
1258
|
-
require
|
|
1278
|
+
require 'active_job'
|
|
1279
|
+
require 'active_job/queue_adapters'
|
|
1259
1280
|
|
|
1260
|
-
require
|
|
1281
|
+
require 'zeitwerk'
|
|
1261
1282
|
# By passing the flag, we acknowledge the extra directory lib/active_job
|
|
1262
1283
|
# has to be managed by the loader and no warning has to be issued for it.
|
|
1263
1284
|
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
|
@@ -1276,23 +1297,35 @@ The method `Zeitwerk::Loader#dirs` returns an array with the absolute paths of t
|
|
|
1276
1297
|
|
|
1277
1298
|
```ruby
|
|
1278
1299
|
loader = Zeitwerk::Loader.new
|
|
1279
|
-
loader.push_dir(Pathname.new(
|
|
1280
|
-
loader.dirs # => [
|
|
1300
|
+
loader.push_dir(Pathname.new('/foo'))
|
|
1301
|
+
loader.dirs # => ['/foo']
|
|
1281
1302
|
```
|
|
1282
1303
|
|
|
1283
1304
|
This method accepts an optional `namespaces` keyword argument. If truthy, the method returns a hash table instead. Keys are the absolute paths of the root directories as strings. Values are their corresponding namespaces, class or module objects:
|
|
1284
1305
|
|
|
1285
1306
|
```ruby
|
|
1286
1307
|
loader = Zeitwerk::Loader.new
|
|
1287
|
-
loader.push_dir(Pathname.new(
|
|
1288
|
-
loader.push_dir(Pathname.new(
|
|
1289
|
-
loader.dirs(namespaces: true) # => {
|
|
1308
|
+
loader.push_dir(Pathname.new('/foo'))
|
|
1309
|
+
loader.push_dir(Pathname.new('/bar'), namespace: Bar)
|
|
1310
|
+
loader.dirs(namespaces: true) # => { '/foo' => Object, '/bar' => Bar }
|
|
1290
1311
|
```
|
|
1291
1312
|
|
|
1292
1313
|
By default, ignored root directories are filtered out. If you want them included, please pass `ignored: true`.
|
|
1293
1314
|
|
|
1294
1315
|
These collections are read-only. Please add to them with `Zeitwerk::Loader#push_dir`.
|
|
1295
1316
|
|
|
1317
|
+
<a id="markdown-autoloaded-constants" name="autoloaded-constants"></a>
|
|
1318
|
+
#### Autoloaded Constants
|
|
1319
|
+
|
|
1320
|
+
Zeitwerk does not keep track of autoloaded constants to minimize its memory footprint, but you can collect them with `on_load` if you will:
|
|
1321
|
+
|
|
1322
|
+
```ruby
|
|
1323
|
+
autoloaded_cpaths = []
|
|
1324
|
+
loader.on_load do |cpath, _value, _abspath|
|
|
1325
|
+
autoloaded_cpaths << cpath
|
|
1326
|
+
end
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1296
1329
|
<a id="markdown-zeitwerkloadercpath_expected_at" name="zeitwerkloadercpath_expected_at"></a>
|
|
1297
1330
|
#### `Zeitwerk::Loader#cpath_expected_at`
|
|
1298
1331
|
|
|
@@ -1301,18 +1334,18 @@ Given a path as a string or `Pathname` object, `Zeitwerk::Loader#cpath_expected_
|
|
|
1301
1334
|
Some examples, assuming that `app/models` is a root directory:
|
|
1302
1335
|
|
|
1303
1336
|
```ruby
|
|
1304
|
-
loader.cpath_expected_at(
|
|
1305
|
-
loader.cpath_expected_at(
|
|
1306
|
-
loader.cpath_expected_at(
|
|
1307
|
-
loader.cpath_expected_at(
|
|
1337
|
+
loader.cpath_expected_at('app/models') # => 'Object'
|
|
1338
|
+
loader.cpath_expected_at('app/models/user.rb') # => 'User'
|
|
1339
|
+
loader.cpath_expected_at('app/models/hotel') # => 'Hotel'
|
|
1340
|
+
loader.cpath_expected_at('app/models/hotel/billing.rb') # => 'Hotel::Billing'
|
|
1308
1341
|
```
|
|
1309
1342
|
|
|
1310
1343
|
If `collapsed` is a collapsed directory:
|
|
1311
1344
|
|
|
1312
1345
|
```ruby
|
|
1313
|
-
loader.cpath_expected_at(
|
|
1314
|
-
loader.cpath_expected_at(
|
|
1315
|
-
loader.cpath_expected_at(
|
|
1346
|
+
loader.cpath_expected_at('a/b/collapsed/c') # => 'A::B::C'
|
|
1347
|
+
loader.cpath_expected_at('a/b/collapsed') # => 'A::B', edge case
|
|
1348
|
+
loader.cpath_expected_at('a/b') # => 'A::B'
|
|
1316
1349
|
```
|
|
1317
1350
|
|
|
1318
1351
|
If the argument corresponds to an [ignored file or directory](#ignoring-parts-of-the-project), the method returns `nil`. Same if the argument is not managed by the loader.
|
|
@@ -1320,16 +1353,18 @@ If the argument corresponds to an [ignored file or directory](#ignoring-parts-of
|
|
|
1320
1353
|
`Zeitwerk::Error` is raised if the given path does not exist:
|
|
1321
1354
|
|
|
1322
1355
|
```ruby
|
|
1323
|
-
loader.cpath_expected_at(
|
|
1356
|
+
loader.cpath_expected_at('non_existing_file.rb') # => Zeitwerk::Error
|
|
1324
1357
|
```
|
|
1325
1358
|
|
|
1326
1359
|
`Zeitwerk::NameError` is raised if a constant path cannot be derived from it:
|
|
1327
1360
|
|
|
1328
1361
|
```ruby
|
|
1329
|
-
loader.cpath_expected_at(
|
|
1362
|
+
loader.cpath_expected_at('8.rb') # => Zeitwerk::NameError
|
|
1330
1363
|
```
|
|
1331
1364
|
|
|
1332
|
-
This method does not parse file contents and does not guarantee files define the returned constant path.
|
|
1365
|
+
This method does not parse file contents and does not guarantee files define the returned constant path.
|
|
1366
|
+
|
|
1367
|
+
Similarly, this method does not validate the project tree. If the project has conflicting oridinary and nsfiles for the same namespace, for example, the call will return the expected constant path for each of them without raising.
|
|
1333
1368
|
|
|
1334
1369
|
`Zeitwerk::Loader#cpath_expected_at` is designed to be used with individual paths. If you want to know all the expected constant paths in the project, please use `Zeitwerk::Loader#all_expected_cpaths`, documented next.
|
|
1335
1370
|
|
|
@@ -1346,10 +1381,10 @@ For example, if `lib` is the root directory of a gem with the following contents
|
|
|
1346
1381
|
lib/.DS_Store
|
|
1347
1382
|
lib/my_gem.rb
|
|
1348
1383
|
lib/my_gem/version.rb
|
|
1349
|
-
lib/my_gem/ignored.rb
|
|
1350
1384
|
lib/my_gem/drivers/unix.rb
|
|
1351
1385
|
lib/my_gem/drivers/windows.rb
|
|
1352
1386
|
lib/my_gem/collapsed/foo.rb
|
|
1387
|
+
lib/my_gem/ignored.rb
|
|
1353
1388
|
lib/tasks/my_gem.rake
|
|
1354
1389
|
```
|
|
1355
1390
|
|
|
@@ -1357,27 +1392,29 @@ lib/tasks/my_gem.rake
|
|
|
1357
1392
|
|
|
1358
1393
|
```ruby
|
|
1359
1394
|
{
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1395
|
+
'/.../lib' => 'Object',
|
|
1396
|
+
'/.../lib/my_gem.rb' => 'MyGem',
|
|
1397
|
+
'/.../lib/my_gem' => 'MyGem',
|
|
1398
|
+
'/.../lib/my_gem/version.rb' => 'MyGem::VERSION',
|
|
1399
|
+
'/.../lib/my_gem/drivers' => 'MyGem::Drivers',
|
|
1400
|
+
'/.../lib/my_gem/drivers/unix.rb' => 'MyGem::Drivers::Unix',
|
|
1401
|
+
'/.../lib/my_gem/drivers/windows.rb' => 'MyGem::Drivers::Windows',
|
|
1402
|
+
'/.../lib/my_gem/collapsed' => 'MyGem',
|
|
1403
|
+
'/.../lib/my_gem/collapsed/foo.rb' => 'MyGem::Foo'
|
|
1369
1404
|
}
|
|
1370
1405
|
```
|
|
1371
1406
|
|
|
1372
1407
|
In the previous example we assume `lib/my_gem/ignored.rb` is ignored, and therefore it is not present in the returned hash. Also, `lib/my_gem/collapsed` is a collapsed directory, so the expected namespace at that level is still `MyGem` (this is an edge case).
|
|
1373
1408
|
|
|
1374
|
-
The file `lib/.DS_Store` is hidden, hence excluded. The directory `lib/tasks` is also
|
|
1409
|
+
The file `lib/.DS_Store` is hidden, hence excluded. The directory `lib/tasks` is also excluded because it contains no files with extension ".rb".
|
|
1375
1410
|
|
|
1376
1411
|
Directory paths do not have trailing slashes.
|
|
1377
1412
|
|
|
1378
1413
|
The order of the hash entries is undefined.
|
|
1379
1414
|
|
|
1380
|
-
This method does not parse
|
|
1415
|
+
This method does not parse file contents and does not guarantee files define the returned constant path.
|
|
1416
|
+
|
|
1417
|
+
Similarly, this method does not validate the project tree. If the project has conflicting oridinary and nsfiles for the same namespace, for example, the call will return the expected constant path for each of them without raising.
|
|
1381
1418
|
|
|
1382
1419
|
<a id="markdown-encodings" name="encodings"></a>
|
|
1383
1420
|
### Encodings
|
|
@@ -1406,15 +1443,6 @@ The test suite passes on Windows with codepage `Windows-1252` if all the involve
|
|
|
1406
1443
|
|
|
1407
1444
|
6. In a given process, ideally, there should be at most one loader with reloading enabled. Technically, you can have more, but it may get tricky if one refers to constants managed by the other one. Do that only if you know what you are doing.
|
|
1408
1445
|
|
|
1409
|
-
<a id="markdown-debuggers" name="debuggers"></a>
|
|
1410
|
-
### Debuggers
|
|
1411
|
-
|
|
1412
|
-
Zeitwerk and [debug.rb](https://github.com/ruby/debug) are fully compatible if CRuby is ≥ 3.1 (see [ruby/debug#558](https://github.com/ruby/debug/pull/558)).
|
|
1413
|
-
|
|
1414
|
-
[Byebug](https://github.com/deivid-rodriguez/byebug) is compatible except for an edge case explained in [deivid-rodriguez/byebug#564](https://github.com/deivid-rodriguez/byebug/issues/564). Prior to CRuby 3.1, `debug.rb` has a similar edge incompatibility.
|
|
1415
|
-
|
|
1416
|
-
[Break](https://github.com/gsamokovarov/break) is fully compatible.
|
|
1417
|
-
|
|
1418
1446
|
<a id="markdown-pronunciation" name="pronunciation"></a>
|
|
1419
1447
|
## Pronunciation
|
|
1420
1448
|
|
|
@@ -1423,9 +1451,12 @@ Zeitwerk and [debug.rb](https://github.com/ruby/debug) are fully compatible if C
|
|
|
1423
1451
|
<a id="markdown-supported-ruby-versions" name="supported-ruby-versions"></a>
|
|
1424
1452
|
## Supported Ruby versions
|
|
1425
1453
|
|
|
1426
|
-
|
|
1454
|
+
Starting with version 2.7, Zeitwerk requires Ruby 3.2 or newer.
|
|
1427
1455
|
|
|
1428
|
-
|
|
1456
|
+
Zeitwerk 2.7 requires TruffleRuby 24.1.2+ due to https://github.com/oracle/truffleruby/issues/3683.
|
|
1457
|
+
Alternatively, TruffleRuby users can use a `< 2.7` version constraint for the `zeitwerk` gem.
|
|
1458
|
+
As of this writing, [autoloading is not fully thread-safe yet on TruffleRuby](https://github.com/oracle/truffleruby/issues/2431).
|
|
1459
|
+
If your program is multi-threaded, you need to eager load before threads are created.
|
|
1429
1460
|
|
|
1430
1461
|
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.)
|
|
1431
1462
|
|
|
@@ -1444,12 +1475,18 @@ To run one particular suite, pass its file name as an argument:
|
|
|
1444
1475
|
bin/test test/lib/zeitwerk/test_eager_load.rb
|
|
1445
1476
|
```
|
|
1446
1477
|
|
|
1478
|
+
That also accepts a line number:
|
|
1479
|
+
|
|
1480
|
+
```
|
|
1481
|
+
bin/test test/lib/zeitwerk/test_eager_load.rb:52
|
|
1482
|
+
```
|
|
1483
|
+
|
|
1447
1484
|
Furthermore, the project has a development dependency on [`minitest-focus`](https://github.com/seattlerb/minitest-focus). To run an individual test mark it with `focus`:
|
|
1448
1485
|
|
|
1449
1486
|
```ruby
|
|
1450
1487
|
focus
|
|
1451
|
-
test
|
|
1452
|
-
assert_equal
|
|
1488
|
+
test 'capitalizes the first letter' do
|
|
1489
|
+
assert_equal 'User', camelize('user')
|
|
1453
1490
|
end
|
|
1454
1491
|
```
|
|
1455
1492
|
|
|
@@ -1463,7 +1500,7 @@ and run `bin/test`.
|
|
|
1463
1500
|
|
|
1464
1501
|
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.
|
|
1465
1502
|
|
|
1466
|
-
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
|
|
1503
|
+
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.
|
|
1467
1504
|
|
|
1468
1505
|
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.
|
|
1469
1506
|
|
|
@@ -19,10 +19,10 @@ module Kernel
|
|
|
19
19
|
alias_method :zeitwerk_original_require, :require
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
#: (String) -> bool
|
|
23
23
|
def require(path)
|
|
24
|
-
if loader = Zeitwerk::Registry.
|
|
25
|
-
if path.end_with?(
|
|
24
|
+
if loader = Zeitwerk::Registry.autoloads.registered?(path)
|
|
25
|
+
if path.end_with?('.rb')
|
|
26
26
|
required = zeitwerk_original_require(path)
|
|
27
27
|
loader.__on_file_autoloaded(path) if required
|
|
28
28
|
required
|
|
@@ -34,7 +34,7 @@ module Kernel
|
|
|
34
34
|
required = zeitwerk_original_require(path)
|
|
35
35
|
if required
|
|
36
36
|
abspath = $LOADED_FEATURES.last
|
|
37
|
-
if loader = Zeitwerk::Registry.
|
|
37
|
+
if loader = Zeitwerk::Registry.autoloads.registered?(abspath)
|
|
38
38
|
loader.__on_file_autoloaded(abspath)
|
|
39
39
|
end
|
|
40
40
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zeitwerk::ConstAdded # :nodoc:
|
|
4
|
+
#: (Symbol) -> void
|
|
5
|
+
def const_added(cname)
|
|
6
|
+
if loader = Zeitwerk::Registry.explicit_namespaces.loader_for(self, cname)
|
|
7
|
+
namespace = const_get(cname, false)
|
|
8
|
+
cref = Zeitwerk::Cref.new(self, cname)
|
|
9
|
+
|
|
10
|
+
unless namespace.is_a?(Module)
|
|
11
|
+
raise Zeitwerk::Error, "#{cref} is expected to be a namespace, should be a class or module (got #{namespace.class})"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
loader.__on_namespace_loaded(cref, namespace)
|
|
15
|
+
end
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Module.prepend(self)
|
|
20
|
+
end
|