zeitwerk 2.7.5 → 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 +131 -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 +64 -18
- data/lib/zeitwerk/loader/eager_load.rb +23 -27
- data/lib/zeitwerk/loader/file_system.rb +72 -25
- data/lib/zeitwerk/loader/helpers.rb +2 -2
- data/lib/zeitwerk/loader.rb +164 -126
- data/lib/zeitwerk/registry.rb +6 -6
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +13 -13
- metadata +1 -1
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)
|
|
@@ -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,54 @@ 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 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.
|
|
288
344
|
|
|
289
345
|
<a id="markdown-collapsing-directories" name="collapsing-directories"></a>
|
|
290
346
|
### Collapsing directories
|
|
@@ -372,9 +428,9 @@ Conceptually, `for_gem` translates to:
|
|
|
372
428
|
```ruby
|
|
373
429
|
# lib/my_gem.rb
|
|
374
430
|
|
|
375
|
-
require
|
|
431
|
+
require 'zeitwerk'
|
|
376
432
|
loader = Zeitwerk::Loader.new
|
|
377
|
-
loader.tag = File.basename(__FILE__,
|
|
433
|
+
loader.tag = File.basename(__FILE__, '.rb')
|
|
378
434
|
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
|
379
435
|
loader.push_dir(File.dirname(__FILE__))
|
|
380
436
|
```
|
|
@@ -384,7 +440,7 @@ If the main module references project constants at the top-level, Zeitwerk has t
|
|
|
384
440
|
```ruby
|
|
385
441
|
# lib/my_gem.rb (main file)
|
|
386
442
|
|
|
387
|
-
require
|
|
443
|
+
require 'zeitwerk'
|
|
388
444
|
loader = Zeitwerk::Loader.for_gem
|
|
389
445
|
loader.setup
|
|
390
446
|
|
|
@@ -430,7 +486,7 @@ Let's suppose you are writing a gem to extend `Net::HTTP` with some niche featur
|
|
|
430
486
|
The top-level file mentioned in the last point is optional. In particular, from
|
|
431
487
|
|
|
432
488
|
```ruby
|
|
433
|
-
gem
|
|
489
|
+
gem 'net-http-niche_feature'
|
|
434
490
|
```
|
|
435
491
|
|
|
436
492
|
if the hyphenated file does not exist, Bundler notes the conventional hyphenated pattern and issues a `require` for `net/http/niche_feature`.
|
|
@@ -443,13 +499,13 @@ The structure of the gem would be like this:
|
|
|
443
499
|
# lib/net-http-niche_feature.rb (optional)
|
|
444
500
|
|
|
445
501
|
# For technical reasons, this cannot be require_relative.
|
|
446
|
-
require
|
|
502
|
+
require 'net/http/niche_feature'
|
|
447
503
|
|
|
448
504
|
|
|
449
505
|
# lib/net/http/niche_feature.rb
|
|
450
506
|
|
|
451
|
-
require
|
|
452
|
-
require
|
|
507
|
+
require 'net/http'
|
|
508
|
+
require 'zeitwerk'
|
|
453
509
|
|
|
454
510
|
loader = Zeitwerk::Loader.for_gem_extension(Net::HTTP)
|
|
455
511
|
loader.setup
|
|
@@ -464,7 +520,7 @@ end
|
|
|
464
520
|
# lib/net/http/niche_feature/version.rb
|
|
465
521
|
|
|
466
522
|
module Net::HTTP::NicheFeature
|
|
467
|
-
VERSION =
|
|
523
|
+
VERSION = '1.0.0'
|
|
468
524
|
end
|
|
469
525
|
```
|
|
470
526
|
|
|
@@ -486,7 +542,7 @@ Let's revisit the example above:
|
|
|
486
542
|
```ruby
|
|
487
543
|
# lib/my_gem.rb (main file)
|
|
488
544
|
|
|
489
|
-
require
|
|
545
|
+
require 'zeitwerk'
|
|
490
546
|
loader = Zeitwerk::Loader.for_gem
|
|
491
547
|
loader.setup
|
|
492
548
|
|
|
@@ -495,7 +551,7 @@ module MyGem
|
|
|
495
551
|
end
|
|
496
552
|
```
|
|
497
553
|
|
|
498
|
-
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`.
|
|
499
555
|
|
|
500
556
|
If autoloading a file does not define the expected class or module, Zeitwerk raises `Zeitwerk::NameError`, which is a subclass of `NameError`.
|
|
501
557
|
|
|
@@ -682,7 +738,7 @@ In order to reload safely, no other thread can be autoloading or reloading concu
|
|
|
682
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:
|
|
683
739
|
|
|
684
740
|
```ruby
|
|
685
|
-
require
|
|
741
|
+
require 'concurrent/atomic/read_write_lock'
|
|
686
742
|
|
|
687
743
|
MyFramework::RELOAD_RW_LOCK = Concurrent::ReadWriteLock.new
|
|
688
744
|
```
|
|
@@ -727,22 +783,22 @@ The camelize logic can be overridden easily for individual basenames:
|
|
|
727
783
|
|
|
728
784
|
```ruby
|
|
729
785
|
loader.inflector.inflect(
|
|
730
|
-
|
|
731
|
-
|
|
786
|
+
'html_parser' => 'HTMLParser',
|
|
787
|
+
'mysql_adapter' => 'MySQLAdapter'
|
|
732
788
|
)
|
|
733
789
|
```
|
|
734
790
|
|
|
735
791
|
The `inflect` method can be invoked several times if you prefer this other style:
|
|
736
792
|
|
|
737
793
|
```ruby
|
|
738
|
-
loader.inflector.inflect
|
|
739
|
-
loader.inflector.inflect
|
|
794
|
+
loader.inflector.inflect 'html_parser' => 'HTMLParser'
|
|
795
|
+
loader.inflector.inflect 'mysql_adapter' => 'MySQLAdapter'
|
|
740
796
|
```
|
|
741
797
|
|
|
742
798
|
Overrides have to match exactly directory or file (without extension) _basenames_. For example, if you configure
|
|
743
799
|
|
|
744
800
|
```ruby
|
|
745
|
-
loader.inflector.inflect(
|
|
801
|
+
loader.inflector.inflect('xml' => 'XML')
|
|
746
802
|
```
|
|
747
803
|
|
|
748
804
|
then the following constants are expected:
|
|
@@ -757,8 +813,8 @@ As you see, any directory whose basename is exactly `xml`, and any file whose ba
|
|
|
757
813
|
|
|
758
814
|
```ruby
|
|
759
815
|
loader.inflector.inflect(
|
|
760
|
-
|
|
761
|
-
|
|
816
|
+
'xml' => 'XML',
|
|
817
|
+
'xml_parser' => 'XMLParser'
|
|
762
818
|
)
|
|
763
819
|
```
|
|
764
820
|
|
|
@@ -813,7 +869,7 @@ The inflectors that ship with Zeitwerk are deterministic and simple. But you can
|
|
|
813
869
|
class MyInflector < Zeitwerk::Inflector
|
|
814
870
|
def camelize(basename, abspath)
|
|
815
871
|
if basename =~ /\Ahtml_(.*)/
|
|
816
|
-
|
|
872
|
+
'HTML' + super($1, abspath)
|
|
817
873
|
else
|
|
818
874
|
super
|
|
819
875
|
end
|
|
@@ -844,8 +900,8 @@ module MyGem
|
|
|
844
900
|
end
|
|
845
901
|
|
|
846
902
|
# lib/my_gem.rb
|
|
847
|
-
require
|
|
848
|
-
require_relative
|
|
903
|
+
require 'zeitwerk'
|
|
904
|
+
require_relative 'my_gem/inflector'
|
|
849
905
|
|
|
850
906
|
loader = Zeitwerk::Loader.for_gem
|
|
851
907
|
loader.inflector = MyGem::Inflector.new(__FILE__)
|
|
@@ -913,13 +969,13 @@ With `on_load`, it is easy to schedule code at boot time that initializes `endpo
|
|
|
913
969
|
|
|
914
970
|
```ruby
|
|
915
971
|
# config/environments/development.rb
|
|
916
|
-
loader.on_load(
|
|
917
|
-
klass.endpoint =
|
|
972
|
+
loader.on_load('SomeApiClient') do |klass, _abspath|
|
|
973
|
+
klass.endpoint = 'https://api.dev'
|
|
918
974
|
end
|
|
919
975
|
|
|
920
976
|
# config/environments/production.rb
|
|
921
|
-
loader.on_load(
|
|
922
|
-
klass.endpoint =
|
|
977
|
+
loader.on_load('SomeApiClient') do |klass, _abspath|
|
|
978
|
+
klass.endpoint = 'https://api.prod'
|
|
923
979
|
end
|
|
924
980
|
```
|
|
925
981
|
|
|
@@ -963,7 +1019,7 @@ When reloading is enabled, you may occasionally need to execute something before
|
|
|
963
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:
|
|
964
1020
|
|
|
965
1021
|
```ruby
|
|
966
|
-
loader.on_unload(
|
|
1022
|
+
loader.on_unload('Country') do |klass, _abspath|
|
|
967
1023
|
klass.clear_cache
|
|
968
1024
|
end
|
|
969
1025
|
```
|
|
@@ -1048,7 +1104,7 @@ Zeitwerk@9fa54b: autoload set for User, to be loaded from ...
|
|
|
1048
1104
|
By default, a random tag like the one above is assigned, but you can change it:
|
|
1049
1105
|
|
|
1050
1106
|
```
|
|
1051
|
-
loader.tag =
|
|
1107
|
+
loader.tag = 'grep_me'
|
|
1052
1108
|
```
|
|
1053
1109
|
|
|
1054
1110
|
The tag of a loader returned by `for_gem` is the basename of the root file without extension:
|
|
@@ -1104,7 +1160,7 @@ loader.setup
|
|
|
1104
1160
|
Now, that file has to be loaded manually with `require` or `require_relative`:
|
|
1105
1161
|
|
|
1106
1162
|
```ruby
|
|
1107
|
-
require_relative
|
|
1163
|
+
require_relative 'my_gem/core_ext/kernel'
|
|
1108
1164
|
```
|
|
1109
1165
|
|
|
1110
1166
|
and you can do that anytime, before configuring the loader, or after configuring the loader, does not matter.
|
|
@@ -1118,7 +1174,7 @@ Let's imagine your project talks to databases, supports several, and has adapter
|
|
|
1118
1174
|
|
|
1119
1175
|
```ruby
|
|
1120
1176
|
# my_gem/db_adapters/postgresql.rb
|
|
1121
|
-
require
|
|
1177
|
+
require 'pg'
|
|
1122
1178
|
```
|
|
1123
1179
|
|
|
1124
1180
|
but you don't want your users to install them all, only the one they are going to use.
|
|
@@ -1156,7 +1212,7 @@ loader.setup
|
|
|
1156
1212
|
In Ruby, if you have several files called `foo.rb` in different directories of `$LOAD_PATH` and execute
|
|
1157
1213
|
|
|
1158
1214
|
```ruby
|
|
1159
|
-
require
|
|
1215
|
+
require 'foo'
|
|
1160
1216
|
```
|
|
1161
1217
|
|
|
1162
1218
|
the first one found gets loaded, and the rest are ignored.
|
|
@@ -1225,10 +1281,10 @@ In order to do so, you need to make sure those modules are loaded before calling
|
|
|
1225
1281
|
|
|
1226
1282
|
```ruby
|
|
1227
1283
|
# Ensure these namespaces are reopened, not defined.
|
|
1228
|
-
require
|
|
1229
|
-
require
|
|
1284
|
+
require 'active_job'
|
|
1285
|
+
require 'active_job/queue_adapters'
|
|
1230
1286
|
|
|
1231
|
-
require
|
|
1287
|
+
require 'zeitwerk'
|
|
1232
1288
|
# By passing the flag, we acknowledge the extra directory lib/active_job
|
|
1233
1289
|
# has to be managed by the loader and no warning has to be issued for it.
|
|
1234
1290
|
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
|
@@ -1247,17 +1303,17 @@ The method `Zeitwerk::Loader#dirs` returns an array with the absolute paths of t
|
|
|
1247
1303
|
|
|
1248
1304
|
```ruby
|
|
1249
1305
|
loader = Zeitwerk::Loader.new
|
|
1250
|
-
loader.push_dir(Pathname.new(
|
|
1251
|
-
loader.dirs # => [
|
|
1306
|
+
loader.push_dir(Pathname.new('/foo'))
|
|
1307
|
+
loader.dirs # => ['/foo']
|
|
1252
1308
|
```
|
|
1253
1309
|
|
|
1254
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:
|
|
1255
1311
|
|
|
1256
1312
|
```ruby
|
|
1257
1313
|
loader = Zeitwerk::Loader.new
|
|
1258
|
-
loader.push_dir(Pathname.new(
|
|
1259
|
-
loader.push_dir(Pathname.new(
|
|
1260
|
-
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 }
|
|
1261
1317
|
```
|
|
1262
1318
|
|
|
1263
1319
|
By default, ignored root directories are filtered out. If you want them included, please pass `ignored: true`.
|
|
@@ -1284,18 +1340,18 @@ Given a path as a string or `Pathname` object, `Zeitwerk::Loader#cpath_expected_
|
|
|
1284
1340
|
Some examples, assuming that `app/models` is a root directory:
|
|
1285
1341
|
|
|
1286
1342
|
```ruby
|
|
1287
|
-
loader.cpath_expected_at(
|
|
1288
|
-
loader.cpath_expected_at(
|
|
1289
|
-
loader.cpath_expected_at(
|
|
1290
|
-
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'
|
|
1291
1347
|
```
|
|
1292
1348
|
|
|
1293
1349
|
If `collapsed` is a collapsed directory:
|
|
1294
1350
|
|
|
1295
1351
|
```ruby
|
|
1296
|
-
loader.cpath_expected_at(
|
|
1297
|
-
loader.cpath_expected_at(
|
|
1298
|
-
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'
|
|
1299
1355
|
```
|
|
1300
1356
|
|
|
1301
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.
|
|
@@ -1303,16 +1359,18 @@ If the argument corresponds to an [ignored file or directory](#ignoring-parts-of
|
|
|
1303
1359
|
`Zeitwerk::Error` is raised if the given path does not exist:
|
|
1304
1360
|
|
|
1305
1361
|
```ruby
|
|
1306
|
-
loader.cpath_expected_at(
|
|
1362
|
+
loader.cpath_expected_at('non_existing_file.rb') # => Zeitwerk::Error
|
|
1307
1363
|
```
|
|
1308
1364
|
|
|
1309
1365
|
`Zeitwerk::NameError` is raised if a constant path cannot be derived from it:
|
|
1310
1366
|
|
|
1311
1367
|
```ruby
|
|
1312
|
-
loader.cpath_expected_at(
|
|
1368
|
+
loader.cpath_expected_at('8.rb') # => Zeitwerk::NameError
|
|
1313
1369
|
```
|
|
1314
1370
|
|
|
1315
|
-
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.
|
|
1316
1374
|
|
|
1317
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.
|
|
1318
1376
|
|
|
@@ -1329,10 +1387,10 @@ For example, if `lib` is the root directory of a gem with the following contents
|
|
|
1329
1387
|
lib/.DS_Store
|
|
1330
1388
|
lib/my_gem.rb
|
|
1331
1389
|
lib/my_gem/version.rb
|
|
1332
|
-
lib/my_gem/ignored.rb
|
|
1333
1390
|
lib/my_gem/drivers/unix.rb
|
|
1334
1391
|
lib/my_gem/drivers/windows.rb
|
|
1335
1392
|
lib/my_gem/collapsed/foo.rb
|
|
1393
|
+
lib/my_gem/ignored.rb
|
|
1336
1394
|
lib/tasks/my_gem.rake
|
|
1337
1395
|
```
|
|
1338
1396
|
|
|
@@ -1340,27 +1398,29 @@ lib/tasks/my_gem.rake
|
|
|
1340
1398
|
|
|
1341
1399
|
```ruby
|
|
1342
1400
|
{
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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'
|
|
1352
1410
|
}
|
|
1353
1411
|
```
|
|
1354
1412
|
|
|
1355
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).
|
|
1356
1414
|
|
|
1357
|
-
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".
|
|
1358
1416
|
|
|
1359
1417
|
Directory paths do not have trailing slashes.
|
|
1360
1418
|
|
|
1361
1419
|
The order of the hash entries is undefined.
|
|
1362
1420
|
|
|
1363
|
-
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.
|
|
1364
1424
|
|
|
1365
1425
|
<a id="markdown-encodings" name="encodings"></a>
|
|
1366
1426
|
### Encodings
|
|
@@ -1431,8 +1491,8 @@ Furthermore, the project has a development dependency on [`minitest-focus`](http
|
|
|
1431
1491
|
|
|
1432
1492
|
```ruby
|
|
1433
1493
|
focus
|
|
1434
|
-
test
|
|
1435
|
-
assert_equal
|
|
1494
|
+
test 'capitalizes the first letter' do
|
|
1495
|
+
assert_equal 'User', camelize('user')
|
|
1436
1496
|
end
|
|
1437
1497
|
```
|
|
1438
1498
|
|
|
@@ -1446,7 +1506,7 @@ and run `bin/test`.
|
|
|
1446
1506
|
|
|
1447
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.
|
|
1448
1508
|
|
|
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
|
|
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.
|
|
1450
1510
|
|
|
1451
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.
|
|
1452
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,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
|