zeitwerk 2.6.18 → 2.8.1

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