zeitwerk 2.7.4 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +150 -71
- data/lib/zeitwerk/core_ext/kernel.rb +1 -1
- data/lib/zeitwerk/cref/map.rb +1 -1
- data/lib/zeitwerk/cref.rb +8 -1
- data/lib/zeitwerk/error.rb +12 -1
- data/lib/zeitwerk/gem_inflector.rb +3 -3
- data/lib/zeitwerk/gem_loader.rb +6 -6
- data/lib/zeitwerk/inflector.rb +8 -8
- data/lib/zeitwerk/loader/callbacks.rb +4 -4
- data/lib/zeitwerk/loader/config.rb +73 -27
- data/lib/zeitwerk/loader/eager_load.rb +42 -48
- data/lib/zeitwerk/loader/file_system.rb +212 -0
- data/lib/zeitwerk/loader/helpers.rb +2 -101
- data/lib/zeitwerk/loader.rb +195 -162
- data/lib/zeitwerk/real_mod_name.rb +1 -1
- data/lib/zeitwerk/registry/loaders.rb +2 -2
- data/lib/zeitwerk/registry.rb +30 -4
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +13 -13
- metadata +4 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 071170ec551e69be67c517af6e04ae2788d6f81f6451a7da0dfab49be8f0c52b
|
|
4
|
+
data.tar.gz: 96fd655043bee86c5f57567a01b5f5282e315b72bde7e5ca370ee8fd5e7917fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b74298014af2753fbb19ad36f2a5d484cfbe364beb6aa583fe43d1f2cdf2fc3135bd5aaa16bedeb45a4a4ecbae95fc9d2e62aae5b59fd3df7cf5422693a947f
|
|
7
|
+
data.tar.gz: 242710104b8d2fba905cb65d1382080d098f4ff6c3d8b982d1cf53e9ea877612b22ee07b9d3f8f155a17b354e8412f2f350d835e7418f6eef7cf33a324a5ea4a
|
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)
|
|
@@ -58,6 +60,7 @@
|
|
|
58
60
|
- [Reopening third-party namespaces](#reopening-third-party-namespaces)
|
|
59
61
|
- [Introspection](#introspection)
|
|
60
62
|
- [`Zeitwerk::Loader#dirs`](#zeitwerkloaderdirs)
|
|
63
|
+
- [Autoloaded Constants](#autoloaded-constants)
|
|
61
64
|
- [`Zeitwerk::Loader#cpath_expected_at`](#zeitwerkloadercpath_expected_at)
|
|
62
65
|
- [`Zeitwerk::Loader#all_expected_cpaths`](#zeitwerkloaderall_expected_cpaths)
|
|
63
66
|
- [Encodings](#encodings)
|
|
@@ -97,7 +100,7 @@ Main interface for gems:
|
|
|
97
100
|
```ruby
|
|
98
101
|
# lib/my_gem.rb (main file)
|
|
99
102
|
|
|
100
|
-
require
|
|
103
|
+
require 'zeitwerk'
|
|
101
104
|
loader = Zeitwerk::Loader.for_gem
|
|
102
105
|
loader.setup # ready!
|
|
103
106
|
|
|
@@ -180,7 +183,7 @@ end
|
|
|
180
183
|
The first example needs a custom [inflection](#inflection) rule:
|
|
181
184
|
|
|
182
185
|
```ruby
|
|
183
|
-
loader.inflector.inflect(
|
|
186
|
+
loader.inflector.inflect('max_retries' => 'MAX_RETRIES')
|
|
184
187
|
```
|
|
185
188
|
|
|
186
189
|
Otherwise, Zeitwerk would expect the file to define `MaxRetries`.
|
|
@@ -219,8 +222,8 @@ Although `Object` is the most common root namespace, you have the flexibility to
|
|
|
219
222
|
For example, given:
|
|
220
223
|
|
|
221
224
|
```ruby
|
|
222
|
-
require
|
|
223
|
-
require
|
|
225
|
+
require 'active_job'
|
|
226
|
+
require 'active_job/queue_adapters'
|
|
224
227
|
loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
|
|
225
228
|
```
|
|
226
229
|
|
|
@@ -263,16 +266,23 @@ To trigger this behavior, the directory must contain non-ignored Ruby files with
|
|
|
263
266
|
<a id="markdown-explicit-namespaces" name="explicit-namespaces"></a>
|
|
264
267
|
### Explicit namespaces
|
|
265
268
|
|
|
266
|
-
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:
|
|
267
275
|
|
|
268
276
|
```
|
|
269
277
|
app/models/hotel.rb -> Hotel
|
|
270
278
|
app/models/hotel/pricing.rb -> Hotel::Pricing
|
|
271
279
|
```
|
|
272
280
|
|
|
273
|
-
|
|
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`.
|
|
282
|
+
|
|
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.
|
|
274
284
|
|
|
275
|
-
The classes and modules from
|
|
285
|
+
The classes and modules from an explicit namespace are already available in the body of the class or module that defines it:
|
|
276
286
|
|
|
277
287
|
```ruby
|
|
278
288
|
class Hotel < ApplicationRecord
|
|
@@ -283,7 +293,54 @@ end
|
|
|
283
293
|
|
|
284
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.
|
|
285
295
|
|
|
286
|
-
|
|
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 styles are not exclusive. Some parts of the project 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
|
+
A project file whose basename is equal to the nsfile is always considered to be an nsfile. 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`.
|
|
338
|
+
|
|
339
|
+
While configurable, `ns.rb` is the recommended convention:
|
|
340
|
+
|
|
341
|
+
* `ns.rb` is short.
|
|
342
|
+
* `ns.rb` suggests "namespace".
|
|
343
|
+
* Needing an `Ns` constant is unlikely.
|
|
287
344
|
|
|
288
345
|
<a id="markdown-collapsing-directories" name="collapsing-directories"></a>
|
|
289
346
|
### Collapsing directories
|
|
@@ -371,9 +428,9 @@ Conceptually, `for_gem` translates to:
|
|
|
371
428
|
```ruby
|
|
372
429
|
# lib/my_gem.rb
|
|
373
430
|
|
|
374
|
-
require
|
|
431
|
+
require 'zeitwerk'
|
|
375
432
|
loader = Zeitwerk::Loader.new
|
|
376
|
-
loader.tag = File.basename(__FILE__,
|
|
433
|
+
loader.tag = File.basename(__FILE__, '.rb')
|
|
377
434
|
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
|
378
435
|
loader.push_dir(File.dirname(__FILE__))
|
|
379
436
|
```
|
|
@@ -383,7 +440,7 @@ If the main module references project constants at the top-level, Zeitwerk has t
|
|
|
383
440
|
```ruby
|
|
384
441
|
# lib/my_gem.rb (main file)
|
|
385
442
|
|
|
386
|
-
require
|
|
443
|
+
require 'zeitwerk'
|
|
387
444
|
loader = Zeitwerk::Loader.for_gem
|
|
388
445
|
loader.setup
|
|
389
446
|
|
|
@@ -429,7 +486,7 @@ Let's suppose you are writing a gem to extend `Net::HTTP` with some niche featur
|
|
|
429
486
|
The top-level file mentioned in the last point is optional. In particular, from
|
|
430
487
|
|
|
431
488
|
```ruby
|
|
432
|
-
gem
|
|
489
|
+
gem 'net-http-niche_feature'
|
|
433
490
|
```
|
|
434
491
|
|
|
435
492
|
if the hyphenated file does not exist, Bundler notes the conventional hyphenated pattern and issues a `require` for `net/http/niche_feature`.
|
|
@@ -442,13 +499,13 @@ The structure of the gem would be like this:
|
|
|
442
499
|
# lib/net-http-niche_feature.rb (optional)
|
|
443
500
|
|
|
444
501
|
# For technical reasons, this cannot be require_relative.
|
|
445
|
-
require
|
|
502
|
+
require 'net/http/niche_feature'
|
|
446
503
|
|
|
447
504
|
|
|
448
505
|
# lib/net/http/niche_feature.rb
|
|
449
506
|
|
|
450
|
-
require
|
|
451
|
-
require
|
|
507
|
+
require 'net/http'
|
|
508
|
+
require 'zeitwerk'
|
|
452
509
|
|
|
453
510
|
loader = Zeitwerk::Loader.for_gem_extension(Net::HTTP)
|
|
454
511
|
loader.setup
|
|
@@ -463,7 +520,7 @@ end
|
|
|
463
520
|
# lib/net/http/niche_feature/version.rb
|
|
464
521
|
|
|
465
522
|
module Net::HTTP::NicheFeature
|
|
466
|
-
VERSION =
|
|
523
|
+
VERSION = '1.0.0'
|
|
467
524
|
end
|
|
468
525
|
```
|
|
469
526
|
|
|
@@ -485,7 +542,7 @@ Let's revisit the example above:
|
|
|
485
542
|
```ruby
|
|
486
543
|
# lib/my_gem.rb (main file)
|
|
487
544
|
|
|
488
|
-
require
|
|
545
|
+
require 'zeitwerk'
|
|
489
546
|
loader = Zeitwerk::Loader.for_gem
|
|
490
547
|
loader.setup
|
|
491
548
|
|
|
@@ -494,7 +551,7 @@ module MyGem
|
|
|
494
551
|
end
|
|
495
552
|
```
|
|
496
553
|
|
|
497
|
-
That works, and there is no `require
|
|
554
|
+
That works, and there is no `require 'my_gem/my_logger'`. When `(*)` is reached, Zeitwerk seamlessly autoloads `MyGem::MyLogger`.
|
|
498
555
|
|
|
499
556
|
If autoloading a file does not define the expected class or module, Zeitwerk raises `Zeitwerk::NameError`, which is a subclass of `NameError`.
|
|
500
557
|
|
|
@@ -681,7 +738,7 @@ In order to reload safely, no other thread can be autoloading or reloading concu
|
|
|
681
738
|
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
739
|
|
|
683
740
|
```ruby
|
|
684
|
-
require
|
|
741
|
+
require 'concurrent/atomic/read_write_lock'
|
|
685
742
|
|
|
686
743
|
MyFramework::RELOAD_RW_LOCK = Concurrent::ReadWriteLock.new
|
|
687
744
|
```
|
|
@@ -726,22 +783,22 @@ The camelize logic can be overridden easily for individual basenames:
|
|
|
726
783
|
|
|
727
784
|
```ruby
|
|
728
785
|
loader.inflector.inflect(
|
|
729
|
-
|
|
730
|
-
|
|
786
|
+
'html_parser' => 'HTMLParser',
|
|
787
|
+
'mysql_adapter' => 'MySQLAdapter'
|
|
731
788
|
)
|
|
732
789
|
```
|
|
733
790
|
|
|
734
791
|
The `inflect` method can be invoked several times if you prefer this other style:
|
|
735
792
|
|
|
736
793
|
```ruby
|
|
737
|
-
loader.inflector.inflect
|
|
738
|
-
loader.inflector.inflect
|
|
794
|
+
loader.inflector.inflect 'html_parser' => 'HTMLParser'
|
|
795
|
+
loader.inflector.inflect 'mysql_adapter' => 'MySQLAdapter'
|
|
739
796
|
```
|
|
740
797
|
|
|
741
798
|
Overrides have to match exactly directory or file (without extension) _basenames_. For example, if you configure
|
|
742
799
|
|
|
743
800
|
```ruby
|
|
744
|
-
loader.inflector.inflect(
|
|
801
|
+
loader.inflector.inflect('xml' => 'XML')
|
|
745
802
|
```
|
|
746
803
|
|
|
747
804
|
then the following constants are expected:
|
|
@@ -756,8 +813,8 @@ As you see, any directory whose basename is exactly `xml`, and any file whose ba
|
|
|
756
813
|
|
|
757
814
|
```ruby
|
|
758
815
|
loader.inflector.inflect(
|
|
759
|
-
|
|
760
|
-
|
|
816
|
+
'xml' => 'XML',
|
|
817
|
+
'xml_parser' => 'XMLParser'
|
|
761
818
|
)
|
|
762
819
|
```
|
|
763
820
|
|
|
@@ -812,7 +869,7 @@ The inflectors that ship with Zeitwerk are deterministic and simple. But you can
|
|
|
812
869
|
class MyInflector < Zeitwerk::Inflector
|
|
813
870
|
def camelize(basename, abspath)
|
|
814
871
|
if basename =~ /\Ahtml_(.*)/
|
|
815
|
-
|
|
872
|
+
'HTML' + super($1, abspath)
|
|
816
873
|
else
|
|
817
874
|
super
|
|
818
875
|
end
|
|
@@ -843,8 +900,8 @@ module MyGem
|
|
|
843
900
|
end
|
|
844
901
|
|
|
845
902
|
# lib/my_gem.rb
|
|
846
|
-
require
|
|
847
|
-
require_relative
|
|
903
|
+
require 'zeitwerk'
|
|
904
|
+
require_relative 'my_gem/inflector'
|
|
848
905
|
|
|
849
906
|
loader = Zeitwerk::Loader.for_gem
|
|
850
907
|
loader.inflector = MyGem::Inflector.new(__FILE__)
|
|
@@ -912,13 +969,13 @@ With `on_load`, it is easy to schedule code at boot time that initializes `endpo
|
|
|
912
969
|
|
|
913
970
|
```ruby
|
|
914
971
|
# config/environments/development.rb
|
|
915
|
-
loader.on_load(
|
|
916
|
-
klass.endpoint =
|
|
972
|
+
loader.on_load('SomeApiClient') do |klass, _abspath|
|
|
973
|
+
klass.endpoint = 'https://api.dev'
|
|
917
974
|
end
|
|
918
975
|
|
|
919
976
|
# config/environments/production.rb
|
|
920
|
-
loader.on_load(
|
|
921
|
-
klass.endpoint =
|
|
977
|
+
loader.on_load('SomeApiClient') do |klass, _abspath|
|
|
978
|
+
klass.endpoint = 'https://api.prod'
|
|
922
979
|
end
|
|
923
980
|
```
|
|
924
981
|
|
|
@@ -962,7 +1019,7 @@ When reloading is enabled, you may occasionally need to execute something before
|
|
|
962
1019
|
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
1020
|
|
|
964
1021
|
```ruby
|
|
965
|
-
loader.on_unload(
|
|
1022
|
+
loader.on_unload('Country') do |klass, _abspath|
|
|
966
1023
|
klass.clear_cache
|
|
967
1024
|
end
|
|
968
1025
|
```
|
|
@@ -1047,7 +1104,7 @@ Zeitwerk@9fa54b: autoload set for User, to be loaded from ...
|
|
|
1047
1104
|
By default, a random tag like the one above is assigned, but you can change it:
|
|
1048
1105
|
|
|
1049
1106
|
```
|
|
1050
|
-
loader.tag =
|
|
1107
|
+
loader.tag = 'grep_me'
|
|
1051
1108
|
```
|
|
1052
1109
|
|
|
1053
1110
|
The tag of a loader returned by `for_gem` is the basename of the root file without extension:
|
|
@@ -1103,7 +1160,7 @@ loader.setup
|
|
|
1103
1160
|
Now, that file has to be loaded manually with `require` or `require_relative`:
|
|
1104
1161
|
|
|
1105
1162
|
```ruby
|
|
1106
|
-
require_relative
|
|
1163
|
+
require_relative 'my_gem/core_ext/kernel'
|
|
1107
1164
|
```
|
|
1108
1165
|
|
|
1109
1166
|
and you can do that anytime, before configuring the loader, or after configuring the loader, does not matter.
|
|
@@ -1117,7 +1174,7 @@ Let's imagine your project talks to databases, supports several, and has adapter
|
|
|
1117
1174
|
|
|
1118
1175
|
```ruby
|
|
1119
1176
|
# my_gem/db_adapters/postgresql.rb
|
|
1120
|
-
require
|
|
1177
|
+
require 'pg'
|
|
1121
1178
|
```
|
|
1122
1179
|
|
|
1123
1180
|
but you don't want your users to install them all, only the one they are going to use.
|
|
@@ -1155,7 +1212,7 @@ loader.setup
|
|
|
1155
1212
|
In Ruby, if you have several files called `foo.rb` in different directories of `$LOAD_PATH` and execute
|
|
1156
1213
|
|
|
1157
1214
|
```ruby
|
|
1158
|
-
require
|
|
1215
|
+
require 'foo'
|
|
1159
1216
|
```
|
|
1160
1217
|
|
|
1161
1218
|
the first one found gets loaded, and the rest are ignored.
|
|
@@ -1224,10 +1281,10 @@ In order to do so, you need to make sure those modules are loaded before calling
|
|
|
1224
1281
|
|
|
1225
1282
|
```ruby
|
|
1226
1283
|
# Ensure these namespaces are reopened, not defined.
|
|
1227
|
-
require
|
|
1228
|
-
require
|
|
1284
|
+
require 'active_job'
|
|
1285
|
+
require 'active_job/queue_adapters'
|
|
1229
1286
|
|
|
1230
|
-
require
|
|
1287
|
+
require 'zeitwerk'
|
|
1231
1288
|
# By passing the flag, we acknowledge the extra directory lib/active_job
|
|
1232
1289
|
# has to be managed by the loader and no warning has to be issued for it.
|
|
1233
1290
|
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
|
@@ -1246,23 +1303,35 @@ The method `Zeitwerk::Loader#dirs` returns an array with the absolute paths of t
|
|
|
1246
1303
|
|
|
1247
1304
|
```ruby
|
|
1248
1305
|
loader = Zeitwerk::Loader.new
|
|
1249
|
-
loader.push_dir(Pathname.new(
|
|
1250
|
-
loader.dirs # => [
|
|
1306
|
+
loader.push_dir(Pathname.new('/foo'))
|
|
1307
|
+
loader.dirs # => ['/foo']
|
|
1251
1308
|
```
|
|
1252
1309
|
|
|
1253
1310
|
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:
|
|
1254
1311
|
|
|
1255
1312
|
```ruby
|
|
1256
1313
|
loader = Zeitwerk::Loader.new
|
|
1257
|
-
loader.push_dir(Pathname.new(
|
|
1258
|
-
loader.push_dir(Pathname.new(
|
|
1259
|
-
loader.dirs(namespaces: true) # => {
|
|
1314
|
+
loader.push_dir(Pathname.new('/foo'))
|
|
1315
|
+
loader.push_dir(Pathname.new('/bar'), namespace: Bar)
|
|
1316
|
+
loader.dirs(namespaces: true) # => { '/foo' => Object, '/bar' => Bar }
|
|
1260
1317
|
```
|
|
1261
1318
|
|
|
1262
1319
|
By default, ignored root directories are filtered out. If you want them included, please pass `ignored: true`.
|
|
1263
1320
|
|
|
1264
1321
|
These collections are read-only. Please add to them with `Zeitwerk::Loader#push_dir`.
|
|
1265
1322
|
|
|
1323
|
+
<a id="markdown-autoloaded-constants" name="autoloaded-constants"></a>
|
|
1324
|
+
#### Autoloaded Constants
|
|
1325
|
+
|
|
1326
|
+
Zeitwerk does not keep track of autoloaded constants to minimize its memory footprint, but you can collect them with `on_load` if you will:
|
|
1327
|
+
|
|
1328
|
+
```ruby
|
|
1329
|
+
autoloaded_cpaths = []
|
|
1330
|
+
loader.on_load do |cpath, _value, _abspath|
|
|
1331
|
+
autoloaded_cpaths << cpath
|
|
1332
|
+
end
|
|
1333
|
+
```
|
|
1334
|
+
|
|
1266
1335
|
<a id="markdown-zeitwerkloadercpath_expected_at" name="zeitwerkloadercpath_expected_at"></a>
|
|
1267
1336
|
#### `Zeitwerk::Loader#cpath_expected_at`
|
|
1268
1337
|
|
|
@@ -1271,18 +1340,18 @@ Given a path as a string or `Pathname` object, `Zeitwerk::Loader#cpath_expected_
|
|
|
1271
1340
|
Some examples, assuming that `app/models` is a root directory:
|
|
1272
1341
|
|
|
1273
1342
|
```ruby
|
|
1274
|
-
loader.cpath_expected_at(
|
|
1275
|
-
loader.cpath_expected_at(
|
|
1276
|
-
loader.cpath_expected_at(
|
|
1277
|
-
loader.cpath_expected_at(
|
|
1343
|
+
loader.cpath_expected_at('app/models') # => 'Object'
|
|
1344
|
+
loader.cpath_expected_at('app/models/user.rb') # => 'User'
|
|
1345
|
+
loader.cpath_expected_at('app/models/hotel') # => 'Hotel'
|
|
1346
|
+
loader.cpath_expected_at('app/models/hotel/billing.rb') # => 'Hotel::Billing'
|
|
1278
1347
|
```
|
|
1279
1348
|
|
|
1280
1349
|
If `collapsed` is a collapsed directory:
|
|
1281
1350
|
|
|
1282
1351
|
```ruby
|
|
1283
|
-
loader.cpath_expected_at(
|
|
1284
|
-
loader.cpath_expected_at(
|
|
1285
|
-
loader.cpath_expected_at(
|
|
1352
|
+
loader.cpath_expected_at('a/b/collapsed/c') # => 'A::B::C'
|
|
1353
|
+
loader.cpath_expected_at('a/b/collapsed') # => 'A::B', edge case
|
|
1354
|
+
loader.cpath_expected_at('a/b') # => 'A::B'
|
|
1286
1355
|
```
|
|
1287
1356
|
|
|
1288
1357
|
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.
|
|
@@ -1290,16 +1359,18 @@ If the argument corresponds to an [ignored file or directory](#ignoring-parts-of
|
|
|
1290
1359
|
`Zeitwerk::Error` is raised if the given path does not exist:
|
|
1291
1360
|
|
|
1292
1361
|
```ruby
|
|
1293
|
-
loader.cpath_expected_at(
|
|
1362
|
+
loader.cpath_expected_at('non_existing_file.rb') # => Zeitwerk::Error
|
|
1294
1363
|
```
|
|
1295
1364
|
|
|
1296
1365
|
`Zeitwerk::NameError` is raised if a constant path cannot be derived from it:
|
|
1297
1366
|
|
|
1298
1367
|
```ruby
|
|
1299
|
-
loader.cpath_expected_at(
|
|
1368
|
+
loader.cpath_expected_at('8.rb') # => Zeitwerk::NameError
|
|
1300
1369
|
```
|
|
1301
1370
|
|
|
1302
|
-
This method does not parse file contents and does not guarantee files define the returned constant path.
|
|
1371
|
+
This method does not parse file contents and does not guarantee files define the returned constant path.
|
|
1372
|
+
|
|
1373
|
+
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.
|
|
1303
1374
|
|
|
1304
1375
|
`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.
|
|
1305
1376
|
|
|
@@ -1316,10 +1387,10 @@ For example, if `lib` is the root directory of a gem with the following contents
|
|
|
1316
1387
|
lib/.DS_Store
|
|
1317
1388
|
lib/my_gem.rb
|
|
1318
1389
|
lib/my_gem/version.rb
|
|
1319
|
-
lib/my_gem/ignored.rb
|
|
1320
1390
|
lib/my_gem/drivers/unix.rb
|
|
1321
1391
|
lib/my_gem/drivers/windows.rb
|
|
1322
1392
|
lib/my_gem/collapsed/foo.rb
|
|
1393
|
+
lib/my_gem/ignored.rb
|
|
1323
1394
|
lib/tasks/my_gem.rake
|
|
1324
1395
|
```
|
|
1325
1396
|
|
|
@@ -1327,27 +1398,29 @@ lib/tasks/my_gem.rake
|
|
|
1327
1398
|
|
|
1328
1399
|
```ruby
|
|
1329
1400
|
{
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1401
|
+
'/.../lib' => 'Object',
|
|
1402
|
+
'/.../lib/my_gem.rb' => 'MyGem',
|
|
1403
|
+
'/.../lib/my_gem' => 'MyGem',
|
|
1404
|
+
'/.../lib/my_gem/version.rb' => 'MyGem::VERSION',
|
|
1405
|
+
'/.../lib/my_gem/drivers' => 'MyGem::Drivers',
|
|
1406
|
+
'/.../lib/my_gem/drivers/unix.rb' => 'MyGem::Drivers::Unix',
|
|
1407
|
+
'/.../lib/my_gem/drivers/windows.rb' => 'MyGem::Drivers::Windows',
|
|
1408
|
+
'/.../lib/my_gem/collapsed' => 'MyGem',
|
|
1409
|
+
'/.../lib/my_gem/collapsed/foo.rb' => 'MyGem::Foo'
|
|
1339
1410
|
}
|
|
1340
1411
|
```
|
|
1341
1412
|
|
|
1342
1413
|
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).
|
|
1343
1414
|
|
|
1344
|
-
The file `lib/.DS_Store` is hidden, hence excluded. The directory `lib/tasks` is also
|
|
1415
|
+
The file `lib/.DS_Store` is hidden, hence excluded. The directory `lib/tasks` is also excluded because it contains no files with extension ".rb".
|
|
1345
1416
|
|
|
1346
1417
|
Directory paths do not have trailing slashes.
|
|
1347
1418
|
|
|
1348
1419
|
The order of the hash entries is undefined.
|
|
1349
1420
|
|
|
1350
|
-
This method does not parse
|
|
1421
|
+
This method does not parse file contents and does not guarantee files define the returned constant path.
|
|
1422
|
+
|
|
1423
|
+
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.
|
|
1351
1424
|
|
|
1352
1425
|
<a id="markdown-encodings" name="encodings"></a>
|
|
1353
1426
|
### Encodings
|
|
@@ -1408,12 +1481,18 @@ To run one particular suite, pass its file name as an argument:
|
|
|
1408
1481
|
bin/test test/lib/zeitwerk/test_eager_load.rb
|
|
1409
1482
|
```
|
|
1410
1483
|
|
|
1484
|
+
That also accepts a line number:
|
|
1485
|
+
|
|
1486
|
+
```
|
|
1487
|
+
bin/test test/lib/zeitwerk/test_eager_load.rb:52
|
|
1488
|
+
```
|
|
1489
|
+
|
|
1411
1490
|
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`:
|
|
1412
1491
|
|
|
1413
1492
|
```ruby
|
|
1414
1493
|
focus
|
|
1415
|
-
test
|
|
1416
|
-
assert_equal
|
|
1494
|
+
test 'capitalizes the first letter' do
|
|
1495
|
+
assert_equal 'User', camelize('user')
|
|
1417
1496
|
end
|
|
1418
1497
|
```
|
|
1419
1498
|
|
|
@@ -1427,7 +1506,7 @@ and run `bin/test`.
|
|
|
1427
1506
|
|
|
1428
1507
|
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.
|
|
1429
1508
|
|
|
1430
|
-
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
|
|
1509
|
+
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.
|
|
1431
1510
|
|
|
1432
1511
|
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.
|
|
1433
1512
|
|
|
@@ -22,7 +22,7 @@ module Kernel
|
|
|
22
22
|
#: (String) -> bool
|
|
23
23
|
def require(path)
|
|
24
24
|
if loader = Zeitwerk::Registry.autoloads.registered?(path)
|
|
25
|
-
if path.end_with?(
|
|
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
|
data/lib/zeitwerk/cref/map.rb
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
# 1. We could also use a 1-level hash whose keys are constant paths. In the
|
|
28
28
|
# example above it would be:
|
|
29
29
|
#
|
|
30
|
-
# {
|
|
30
|
+
# { 'M::X' => 0, 'M::Y' => 1, 'N::Z' => 2 }
|
|
31
31
|
#
|
|
32
32
|
# The gem used this approach for several years.
|
|
33
33
|
#
|
data/lib/zeitwerk/cref.rb
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
#
|
|
12
12
|
# The constant may or may not exist in `mod`.
|
|
13
13
|
class Zeitwerk::Cref
|
|
14
|
-
require_relative
|
|
14
|
+
require_relative 'cref/map'
|
|
15
15
|
|
|
16
16
|
include Zeitwerk::RealModName
|
|
17
17
|
|
|
@@ -66,4 +66,11 @@ class Zeitwerk::Cref
|
|
|
66
66
|
def remove
|
|
67
67
|
@mod.__send__(:remove_const, @cname)
|
|
68
68
|
end
|
|
69
|
+
|
|
70
|
+
#: () -> String?
|
|
71
|
+
def location
|
|
72
|
+
if (location = @mod.const_source_location(@cname)) && !location.empty?
|
|
73
|
+
location.join(':')
|
|
74
|
+
end
|
|
75
|
+
end
|
|
69
76
|
end
|
data/lib/zeitwerk/error.rb
CHANGED
|
@@ -17,7 +17,18 @@ module Zeitwerk
|
|
|
17
17
|
class SetupRequired < Error
|
|
18
18
|
#: () -> void
|
|
19
19
|
def initialize
|
|
20
|
-
super(
|
|
20
|
+
super('please, finish your configuration and call Zeitwerk::Loader#setup once all is ready')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class ConflictingNamespaceDefinitionError < Error
|
|
25
|
+
#: (String, location: String?, conflicting_file: String) -> void
|
|
26
|
+
def initialize(cpath, location:, conflicting_file:)
|
|
27
|
+
if location
|
|
28
|
+
super("conflicting namespace definition for #{cpath}: #{conflicting_file} conflicts with #{location}")
|
|
29
|
+
else
|
|
30
|
+
super("conflicting namespace definition for #{cpath}: #{conflicting_file} conflicts with an already defined namespace")
|
|
31
|
+
end
|
|
21
32
|
end
|
|
22
33
|
end
|
|
23
34
|
end
|
|
@@ -4,14 +4,14 @@ module Zeitwerk
|
|
|
4
4
|
class GemInflector < Inflector
|
|
5
5
|
#: (String) -> void
|
|
6
6
|
def initialize(root_file)
|
|
7
|
-
namespace = File.basename(root_file,
|
|
7
|
+
namespace = File.basename(root_file, '.rb')
|
|
8
8
|
root_dir = File.dirname(root_file)
|
|
9
|
-
@version_file = File.join(root_dir, namespace,
|
|
9
|
+
@version_file = File.join(root_dir, namespace, 'version.rb')
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
#: (String, String) -> String
|
|
13
13
|
def camelize(basename, abspath)
|
|
14
|
-
abspath == @version_file ?
|
|
14
|
+
abspath == @version_file ? 'VERSION' : super
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
end
|
data/lib/zeitwerk/gem_loader.rb
CHANGED
|
@@ -19,8 +19,8 @@ module Zeitwerk
|
|
|
19
19
|
def initialize(root_file, namespace:, warn_on_extra_files:)
|
|
20
20
|
super()
|
|
21
21
|
|
|
22
|
-
@tag = File.basename(root_file,
|
|
23
|
-
@tag = real_mod_name(namespace) +
|
|
22
|
+
@tag = File.basename(root_file, '.rb')
|
|
23
|
+
@tag = real_mod_name(namespace) + '-' + @tag unless namespace.equal?(Object)
|
|
24
24
|
|
|
25
25
|
@inflector = GemInflector.new(root_file)
|
|
26
26
|
@root_file = File.expand_path(root_file)
|
|
@@ -40,14 +40,14 @@ module Zeitwerk
|
|
|
40
40
|
|
|
41
41
|
#: () -> void
|
|
42
42
|
def warn_on_extra_files
|
|
43
|
-
expected_namespace_dir = @root_file.delete_suffix(
|
|
43
|
+
expected_namespace_dir = @root_file.delete_suffix('.rb')
|
|
44
44
|
|
|
45
|
-
ls(@root_dir) do |basename, abspath, ftype|
|
|
45
|
+
@fs.ls(@root_dir) do |basename, abspath, ftype|
|
|
46
46
|
next if abspath == @root_file
|
|
47
47
|
next if abspath == expected_namespace_dir
|
|
48
48
|
|
|
49
|
-
basename_without_ext = basename.delete_suffix(
|
|
50
|
-
cname =
|
|
49
|
+
basename_without_ext = basename.delete_suffix('.rb')
|
|
50
|
+
cname = cname_for(basename_without_ext, abspath)
|
|
51
51
|
|
|
52
52
|
warn(<<~EOS)
|
|
53
53
|
WARNING: Zeitwerk defines the constant #{cname} after the #{ftype}
|