zeitwerk 2.7.5 → 2.8.2
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 +125 -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 +4 -4
- data/lib/zeitwerk/inflector.rb +8 -8
- data/lib/zeitwerk/loader/callbacks.rb +1 -1
- data/lib/zeitwerk/loader/config.rb +87 -24
- data/lib/zeitwerk/loader/constant_path_validator.rb +17 -0
- data/lib/zeitwerk/loader/eager_load.rb +23 -27
- data/lib/zeitwerk/loader/file_system.rb +72 -25
- data/lib/zeitwerk/loader/helpers.rb +3 -6
- data/lib/zeitwerk/loader.rb +166 -126
- data/lib/zeitwerk/registry/loaders.rb +2 -2
- data/lib/zeitwerk/registry.rb +6 -6
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +13 -13
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bc67ff7496feaaf2e3598809e67733b9be0f92f69820736e3471ae0487c39d85
|
|
4
|
+
data.tar.gz: cecf1045923045d9d11d4f5e6c214a9e6dd14abd742fc1065c6f23a8aec82e13
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 739fe00bce3542defa99e3c7a9014d7c4e00d0aa5a062e996c00dc2c9139e26de055e5aee806807c6da614504275bf2c790904d323d47213c031c291713daad6
|
|
7
|
+
data.tar.gz: 85acfebcb1de71e4c54e94803fb3be1c804960d3ba3754cdb6903a32a61e52f139cc06f7672f2cdcabb189dc9d66975479fc20a6733152e58e3131797fc151e4
|
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)
|
|
@@ -98,7 +100,7 @@ Main interface for gems:
|
|
|
98
100
|
```ruby
|
|
99
101
|
# lib/my_gem.rb (main file)
|
|
100
102
|
|
|
101
|
-
require
|
|
103
|
+
require 'zeitwerk'
|
|
102
104
|
loader = Zeitwerk::Loader.for_gem
|
|
103
105
|
loader.setup # ready!
|
|
104
106
|
|
|
@@ -181,7 +183,7 @@ end
|
|
|
181
183
|
The first example needs a custom [inflection](#inflection) rule:
|
|
182
184
|
|
|
183
185
|
```ruby
|
|
184
|
-
loader.inflector.inflect(
|
|
186
|
+
loader.inflector.inflect('max_retries' => 'MAX_RETRIES')
|
|
185
187
|
```
|
|
186
188
|
|
|
187
189
|
Otherwise, Zeitwerk would expect the file to define `MaxRetries`.
|
|
@@ -220,8 +222,8 @@ Although `Object` is the most common root namespace, you have the flexibility to
|
|
|
220
222
|
For example, given:
|
|
221
223
|
|
|
222
224
|
```ruby
|
|
223
|
-
require
|
|
224
|
-
require
|
|
225
|
+
require 'active_job'
|
|
226
|
+
require 'active_job/queue_adapters'
|
|
225
227
|
loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
|
|
226
228
|
```
|
|
227
229
|
|
|
@@ -264,16 +266,23 @@ To trigger this behavior, the directory must contain non-ignored Ruby files with
|
|
|
264
266
|
<a id="markdown-explicit-namespaces" name="explicit-namespaces"></a>
|
|
265
267
|
### Explicit namespaces
|
|
266
268
|
|
|
267
|
-
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:
|
|
268
275
|
|
|
269
276
|
```
|
|
270
277
|
app/models/hotel.rb -> Hotel
|
|
271
278
|
app/models/hotel/pricing.rb -> Hotel::Pricing
|
|
272
279
|
```
|
|
273
280
|
|
|
274
|
-
|
|
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`.
|
|
275
282
|
|
|
276
|
-
|
|
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:
|
|
277
286
|
|
|
278
287
|
```ruby
|
|
279
288
|
class Hotel < ApplicationRecord
|
|
@@ -284,7 +293,48 @@ end
|
|
|
284
293
|
|
|
285
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.
|
|
286
295
|
|
|
287
|
-
|
|
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`.
|
|
288
338
|
|
|
289
339
|
<a id="markdown-collapsing-directories" name="collapsing-directories"></a>
|
|
290
340
|
### Collapsing directories
|
|
@@ -372,9 +422,9 @@ Conceptually, `for_gem` translates to:
|
|
|
372
422
|
```ruby
|
|
373
423
|
# lib/my_gem.rb
|
|
374
424
|
|
|
375
|
-
require
|
|
425
|
+
require 'zeitwerk'
|
|
376
426
|
loader = Zeitwerk::Loader.new
|
|
377
|
-
loader.tag = File.basename(__FILE__,
|
|
427
|
+
loader.tag = File.basename(__FILE__, '.rb')
|
|
378
428
|
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
|
379
429
|
loader.push_dir(File.dirname(__FILE__))
|
|
380
430
|
```
|
|
@@ -384,7 +434,7 @@ If the main module references project constants at the top-level, Zeitwerk has t
|
|
|
384
434
|
```ruby
|
|
385
435
|
# lib/my_gem.rb (main file)
|
|
386
436
|
|
|
387
|
-
require
|
|
437
|
+
require 'zeitwerk'
|
|
388
438
|
loader = Zeitwerk::Loader.for_gem
|
|
389
439
|
loader.setup
|
|
390
440
|
|
|
@@ -430,7 +480,7 @@ Let's suppose you are writing a gem to extend `Net::HTTP` with some niche featur
|
|
|
430
480
|
The top-level file mentioned in the last point is optional. In particular, from
|
|
431
481
|
|
|
432
482
|
```ruby
|
|
433
|
-
gem
|
|
483
|
+
gem 'net-http-niche_feature'
|
|
434
484
|
```
|
|
435
485
|
|
|
436
486
|
if the hyphenated file does not exist, Bundler notes the conventional hyphenated pattern and issues a `require` for `net/http/niche_feature`.
|
|
@@ -443,13 +493,13 @@ The structure of the gem would be like this:
|
|
|
443
493
|
# lib/net-http-niche_feature.rb (optional)
|
|
444
494
|
|
|
445
495
|
# For technical reasons, this cannot be require_relative.
|
|
446
|
-
require
|
|
496
|
+
require 'net/http/niche_feature'
|
|
447
497
|
|
|
448
498
|
|
|
449
499
|
# lib/net/http/niche_feature.rb
|
|
450
500
|
|
|
451
|
-
require
|
|
452
|
-
require
|
|
501
|
+
require 'net/http'
|
|
502
|
+
require 'zeitwerk'
|
|
453
503
|
|
|
454
504
|
loader = Zeitwerk::Loader.for_gem_extension(Net::HTTP)
|
|
455
505
|
loader.setup
|
|
@@ -464,7 +514,7 @@ end
|
|
|
464
514
|
# lib/net/http/niche_feature/version.rb
|
|
465
515
|
|
|
466
516
|
module Net::HTTP::NicheFeature
|
|
467
|
-
VERSION =
|
|
517
|
+
VERSION = '1.0.0'
|
|
468
518
|
end
|
|
469
519
|
```
|
|
470
520
|
|
|
@@ -486,7 +536,7 @@ Let's revisit the example above:
|
|
|
486
536
|
```ruby
|
|
487
537
|
# lib/my_gem.rb (main file)
|
|
488
538
|
|
|
489
|
-
require
|
|
539
|
+
require 'zeitwerk'
|
|
490
540
|
loader = Zeitwerk::Loader.for_gem
|
|
491
541
|
loader.setup
|
|
492
542
|
|
|
@@ -495,7 +545,7 @@ module MyGem
|
|
|
495
545
|
end
|
|
496
546
|
```
|
|
497
547
|
|
|
498
|
-
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`.
|
|
499
549
|
|
|
500
550
|
If autoloading a file does not define the expected class or module, Zeitwerk raises `Zeitwerk::NameError`, which is a subclass of `NameError`.
|
|
501
551
|
|
|
@@ -682,7 +732,7 @@ In order to reload safely, no other thread can be autoloading or reloading concu
|
|
|
682
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:
|
|
683
733
|
|
|
684
734
|
```ruby
|
|
685
|
-
require
|
|
735
|
+
require 'concurrent/atomic/read_write_lock'
|
|
686
736
|
|
|
687
737
|
MyFramework::RELOAD_RW_LOCK = Concurrent::ReadWriteLock.new
|
|
688
738
|
```
|
|
@@ -727,22 +777,22 @@ The camelize logic can be overridden easily for individual basenames:
|
|
|
727
777
|
|
|
728
778
|
```ruby
|
|
729
779
|
loader.inflector.inflect(
|
|
730
|
-
|
|
731
|
-
|
|
780
|
+
'html_parser' => 'HTMLParser',
|
|
781
|
+
'mysql_adapter' => 'MySQLAdapter'
|
|
732
782
|
)
|
|
733
783
|
```
|
|
734
784
|
|
|
735
785
|
The `inflect` method can be invoked several times if you prefer this other style:
|
|
736
786
|
|
|
737
787
|
```ruby
|
|
738
|
-
loader.inflector.inflect
|
|
739
|
-
loader.inflector.inflect
|
|
788
|
+
loader.inflector.inflect 'html_parser' => 'HTMLParser'
|
|
789
|
+
loader.inflector.inflect 'mysql_adapter' => 'MySQLAdapter'
|
|
740
790
|
```
|
|
741
791
|
|
|
742
792
|
Overrides have to match exactly directory or file (without extension) _basenames_. For example, if you configure
|
|
743
793
|
|
|
744
794
|
```ruby
|
|
745
|
-
loader.inflector.inflect(
|
|
795
|
+
loader.inflector.inflect('xml' => 'XML')
|
|
746
796
|
```
|
|
747
797
|
|
|
748
798
|
then the following constants are expected:
|
|
@@ -757,8 +807,8 @@ As you see, any directory whose basename is exactly `xml`, and any file whose ba
|
|
|
757
807
|
|
|
758
808
|
```ruby
|
|
759
809
|
loader.inflector.inflect(
|
|
760
|
-
|
|
761
|
-
|
|
810
|
+
'xml' => 'XML',
|
|
811
|
+
'xml_parser' => 'XMLParser'
|
|
762
812
|
)
|
|
763
813
|
```
|
|
764
814
|
|
|
@@ -813,7 +863,7 @@ The inflectors that ship with Zeitwerk are deterministic and simple. But you can
|
|
|
813
863
|
class MyInflector < Zeitwerk::Inflector
|
|
814
864
|
def camelize(basename, abspath)
|
|
815
865
|
if basename =~ /\Ahtml_(.*)/
|
|
816
|
-
|
|
866
|
+
'HTML' + super($1, abspath)
|
|
817
867
|
else
|
|
818
868
|
super
|
|
819
869
|
end
|
|
@@ -844,8 +894,8 @@ module MyGem
|
|
|
844
894
|
end
|
|
845
895
|
|
|
846
896
|
# lib/my_gem.rb
|
|
847
|
-
require
|
|
848
|
-
require_relative
|
|
897
|
+
require 'zeitwerk'
|
|
898
|
+
require_relative 'my_gem/inflector'
|
|
849
899
|
|
|
850
900
|
loader = Zeitwerk::Loader.for_gem
|
|
851
901
|
loader.inflector = MyGem::Inflector.new(__FILE__)
|
|
@@ -913,13 +963,13 @@ With `on_load`, it is easy to schedule code at boot time that initializes `endpo
|
|
|
913
963
|
|
|
914
964
|
```ruby
|
|
915
965
|
# config/environments/development.rb
|
|
916
|
-
loader.on_load(
|
|
917
|
-
klass.endpoint =
|
|
966
|
+
loader.on_load('SomeApiClient') do |klass, _abspath|
|
|
967
|
+
klass.endpoint = 'https://api.dev'
|
|
918
968
|
end
|
|
919
969
|
|
|
920
970
|
# config/environments/production.rb
|
|
921
|
-
loader.on_load(
|
|
922
|
-
klass.endpoint =
|
|
971
|
+
loader.on_load('SomeApiClient') do |klass, _abspath|
|
|
972
|
+
klass.endpoint = 'https://api.prod'
|
|
923
973
|
end
|
|
924
974
|
```
|
|
925
975
|
|
|
@@ -963,7 +1013,7 @@ When reloading is enabled, you may occasionally need to execute something before
|
|
|
963
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:
|
|
964
1014
|
|
|
965
1015
|
```ruby
|
|
966
|
-
loader.on_unload(
|
|
1016
|
+
loader.on_unload('Country') do |klass, _abspath|
|
|
967
1017
|
klass.clear_cache
|
|
968
1018
|
end
|
|
969
1019
|
```
|
|
@@ -1048,7 +1098,7 @@ Zeitwerk@9fa54b: autoload set for User, to be loaded from ...
|
|
|
1048
1098
|
By default, a random tag like the one above is assigned, but you can change it:
|
|
1049
1099
|
|
|
1050
1100
|
```
|
|
1051
|
-
loader.tag =
|
|
1101
|
+
loader.tag = 'grep_me'
|
|
1052
1102
|
```
|
|
1053
1103
|
|
|
1054
1104
|
The tag of a loader returned by `for_gem` is the basename of the root file without extension:
|
|
@@ -1104,7 +1154,7 @@ loader.setup
|
|
|
1104
1154
|
Now, that file has to be loaded manually with `require` or `require_relative`:
|
|
1105
1155
|
|
|
1106
1156
|
```ruby
|
|
1107
|
-
require_relative
|
|
1157
|
+
require_relative 'my_gem/core_ext/kernel'
|
|
1108
1158
|
```
|
|
1109
1159
|
|
|
1110
1160
|
and you can do that anytime, before configuring the loader, or after configuring the loader, does not matter.
|
|
@@ -1118,7 +1168,7 @@ Let's imagine your project talks to databases, supports several, and has adapter
|
|
|
1118
1168
|
|
|
1119
1169
|
```ruby
|
|
1120
1170
|
# my_gem/db_adapters/postgresql.rb
|
|
1121
|
-
require
|
|
1171
|
+
require 'pg'
|
|
1122
1172
|
```
|
|
1123
1173
|
|
|
1124
1174
|
but you don't want your users to install them all, only the one they are going to use.
|
|
@@ -1156,7 +1206,7 @@ loader.setup
|
|
|
1156
1206
|
In Ruby, if you have several files called `foo.rb` in different directories of `$LOAD_PATH` and execute
|
|
1157
1207
|
|
|
1158
1208
|
```ruby
|
|
1159
|
-
require
|
|
1209
|
+
require 'foo'
|
|
1160
1210
|
```
|
|
1161
1211
|
|
|
1162
1212
|
the first one found gets loaded, and the rest are ignored.
|
|
@@ -1225,10 +1275,10 @@ In order to do so, you need to make sure those modules are loaded before calling
|
|
|
1225
1275
|
|
|
1226
1276
|
```ruby
|
|
1227
1277
|
# Ensure these namespaces are reopened, not defined.
|
|
1228
|
-
require
|
|
1229
|
-
require
|
|
1278
|
+
require 'active_job'
|
|
1279
|
+
require 'active_job/queue_adapters'
|
|
1230
1280
|
|
|
1231
|
-
require
|
|
1281
|
+
require 'zeitwerk'
|
|
1232
1282
|
# By passing the flag, we acknowledge the extra directory lib/active_job
|
|
1233
1283
|
# has to be managed by the loader and no warning has to be issued for it.
|
|
1234
1284
|
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
|
@@ -1247,17 +1297,17 @@ The method `Zeitwerk::Loader#dirs` returns an array with the absolute paths of t
|
|
|
1247
1297
|
|
|
1248
1298
|
```ruby
|
|
1249
1299
|
loader = Zeitwerk::Loader.new
|
|
1250
|
-
loader.push_dir(Pathname.new(
|
|
1251
|
-
loader.dirs # => [
|
|
1300
|
+
loader.push_dir(Pathname.new('/foo'))
|
|
1301
|
+
loader.dirs # => ['/foo']
|
|
1252
1302
|
```
|
|
1253
1303
|
|
|
1254
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:
|
|
1255
1305
|
|
|
1256
1306
|
```ruby
|
|
1257
1307
|
loader = Zeitwerk::Loader.new
|
|
1258
|
-
loader.push_dir(Pathname.new(
|
|
1259
|
-
loader.push_dir(Pathname.new(
|
|
1260
|
-
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 }
|
|
1261
1311
|
```
|
|
1262
1312
|
|
|
1263
1313
|
By default, ignored root directories are filtered out. If you want them included, please pass `ignored: true`.
|
|
@@ -1284,18 +1334,18 @@ Given a path as a string or `Pathname` object, `Zeitwerk::Loader#cpath_expected_
|
|
|
1284
1334
|
Some examples, assuming that `app/models` is a root directory:
|
|
1285
1335
|
|
|
1286
1336
|
```ruby
|
|
1287
|
-
loader.cpath_expected_at(
|
|
1288
|
-
loader.cpath_expected_at(
|
|
1289
|
-
loader.cpath_expected_at(
|
|
1290
|
-
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'
|
|
1291
1341
|
```
|
|
1292
1342
|
|
|
1293
1343
|
If `collapsed` is a collapsed directory:
|
|
1294
1344
|
|
|
1295
1345
|
```ruby
|
|
1296
|
-
loader.cpath_expected_at(
|
|
1297
|
-
loader.cpath_expected_at(
|
|
1298
|
-
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'
|
|
1299
1349
|
```
|
|
1300
1350
|
|
|
1301
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.
|
|
@@ -1303,16 +1353,18 @@ If the argument corresponds to an [ignored file or directory](#ignoring-parts-of
|
|
|
1303
1353
|
`Zeitwerk::Error` is raised if the given path does not exist:
|
|
1304
1354
|
|
|
1305
1355
|
```ruby
|
|
1306
|
-
loader.cpath_expected_at(
|
|
1356
|
+
loader.cpath_expected_at('non_existing_file.rb') # => Zeitwerk::Error
|
|
1307
1357
|
```
|
|
1308
1358
|
|
|
1309
1359
|
`Zeitwerk::NameError` is raised if a constant path cannot be derived from it:
|
|
1310
1360
|
|
|
1311
1361
|
```ruby
|
|
1312
|
-
loader.cpath_expected_at(
|
|
1362
|
+
loader.cpath_expected_at('8.rb') # => Zeitwerk::NameError
|
|
1313
1363
|
```
|
|
1314
1364
|
|
|
1315
|
-
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.
|
|
1316
1368
|
|
|
1317
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.
|
|
1318
1370
|
|
|
@@ -1329,10 +1381,10 @@ For example, if `lib` is the root directory of a gem with the following contents
|
|
|
1329
1381
|
lib/.DS_Store
|
|
1330
1382
|
lib/my_gem.rb
|
|
1331
1383
|
lib/my_gem/version.rb
|
|
1332
|
-
lib/my_gem/ignored.rb
|
|
1333
1384
|
lib/my_gem/drivers/unix.rb
|
|
1334
1385
|
lib/my_gem/drivers/windows.rb
|
|
1335
1386
|
lib/my_gem/collapsed/foo.rb
|
|
1387
|
+
lib/my_gem/ignored.rb
|
|
1336
1388
|
lib/tasks/my_gem.rake
|
|
1337
1389
|
```
|
|
1338
1390
|
|
|
@@ -1340,27 +1392,29 @@ lib/tasks/my_gem.rake
|
|
|
1340
1392
|
|
|
1341
1393
|
```ruby
|
|
1342
1394
|
{
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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'
|
|
1352
1404
|
}
|
|
1353
1405
|
```
|
|
1354
1406
|
|
|
1355
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).
|
|
1356
1408
|
|
|
1357
|
-
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".
|
|
1358
1410
|
|
|
1359
1411
|
Directory paths do not have trailing slashes.
|
|
1360
1412
|
|
|
1361
1413
|
The order of the hash entries is undefined.
|
|
1362
1414
|
|
|
1363
|
-
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.
|
|
1364
1418
|
|
|
1365
1419
|
<a id="markdown-encodings" name="encodings"></a>
|
|
1366
1420
|
### Encodings
|
|
@@ -1431,8 +1485,8 @@ Furthermore, the project has a development dependency on [`minitest-focus`](http
|
|
|
1431
1485
|
|
|
1432
1486
|
```ruby
|
|
1433
1487
|
focus
|
|
1434
|
-
test
|
|
1435
|
-
assert_equal
|
|
1488
|
+
test 'capitalizes the first letter' do
|
|
1489
|
+
assert_equal 'User', camelize('user')
|
|
1436
1490
|
end
|
|
1437
1491
|
```
|
|
1438
1492
|
|
|
@@ -1446,7 +1500,7 @@ and run `bin/test`.
|
|
|
1446
1500
|
|
|
1447
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.
|
|
1448
1502
|
|
|
1449
|
-
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.
|
|
1450
1504
|
|
|
1451
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.
|
|
1452
1506
|
|
|
@@ -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,13 +40,13 @@ 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
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(
|
|
49
|
+
basename_without_ext = basename.delete_suffix('.rb')
|
|
50
50
|
cname = cname_for(basename_without_ext, abspath)
|
|
51
51
|
|
|
52
52
|
warn(<<~EOS)
|
data/lib/zeitwerk/inflector.rb
CHANGED
|
@@ -5,9 +5,9 @@ module Zeitwerk
|
|
|
5
5
|
# Very basic snake case -> camel case conversion.
|
|
6
6
|
#
|
|
7
7
|
# inflector = Zeitwerk::Inflector.new
|
|
8
|
-
# inflector.camelize(
|
|
9
|
-
# inflector.camelize(
|
|
10
|
-
# inflector.camelize(
|
|
8
|
+
# inflector.camelize('post', ...) # => 'Post'
|
|
9
|
+
# inflector.camelize('users_controller', ...) # => 'UsersController'
|
|
10
|
+
# inflector.camelize('api', ...) # => 'Api'
|
|
11
11
|
#
|
|
12
12
|
# Takes into account hard-coded mappings configured with `inflect`.
|
|
13
13
|
#
|
|
@@ -20,13 +20,13 @@ module Zeitwerk
|
|
|
20
20
|
#
|
|
21
21
|
# inflector = Zeitwerk::Inflector.new
|
|
22
22
|
# inflector.inflect(
|
|
23
|
-
#
|
|
24
|
-
#
|
|
23
|
+
# 'html_parser' => 'HTMLParser',
|
|
24
|
+
# 'mysql_adapter' => 'MySQLAdapter'
|
|
25
25
|
# )
|
|
26
26
|
#
|
|
27
|
-
# inflector.camelize(
|
|
28
|
-
# inflector.camelize(
|
|
29
|
-
# inflector.camelize(
|
|
27
|
+
# inflector.camelize('html_parser', abspath) # => 'HTMLParser'
|
|
28
|
+
# inflector.camelize('mysql_adapter', abspath) # => 'MySQLAdapter'
|
|
29
|
+
# inflector.camelize('users_controller', abspath) # => 'UsersController'
|
|
30
30
|
#
|
|
31
31
|
#: (Hash[String, String]) -> void
|
|
32
32
|
def inflect(inflections)
|
|
@@ -77,7 +77,7 @@ module Zeitwerk::Loader::Callbacks # :nodoc: all
|
|
|
77
77
|
internal def on_namespace_loaded(cref, namespace)
|
|
78
78
|
if dirs = namespace_dirs.delete(cref)
|
|
79
79
|
dirs.each do |dir|
|
|
80
|
-
define_autoloads_for_dir(dir, namespace)
|
|
80
|
+
define_autoloads_for_dir(dir, namespace, external: false)
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
end
|