zeitwerk 2.2.0 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +204 -31
- data/lib/zeitwerk/inflector.rb +2 -2
- data/lib/zeitwerk/kernel.rb +31 -0
- data/lib/zeitwerk/loader.rb +96 -28
- data/lib/zeitwerk/loader/callbacks.rb +1 -1
- data/lib/zeitwerk/real_mod_name.rb +8 -2
- data/lib/zeitwerk/version.rb +1 -1
- metadata +9 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 682fa352a9d6d4bf68d07cf2cbfbb539de3cfec4e8af2f930894ab27c707ca93
|
4
|
+
data.tar.gz: 1e7ce6157e6046cdb60f0f1b531f1f39fb97cad3537392114be5cf84dae672c9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e308baba1a8fbe4d7c64d7195c753d795a53caefa7264d2404117cb620f2081f6b0fc05c056dafb035b44430102e4472b7eb9fb3a4fd9d70aa124b7f9a3a8313
|
7
|
+
data.tar.gz: 5139d5abaeed86e256493b592460b13b19fd7602f7c099f7017b55af9f16a91429e84074a4e31fe79bf28235e02cbba3055cc4bda579bbc7de2a391c5997a393
|
data/README.md
CHANGED
@@ -12,11 +12,15 @@
|
|
12
12
|
- [File structure](#file-structure)
|
13
13
|
- [Implicit namespaces](#implicit-namespaces)
|
14
14
|
- [Explicit namespaces](#explicit-namespaces)
|
15
|
+
- [Collapsing directories](#collapsing-directories)
|
15
16
|
- [Nested root directories](#nested-root-directories)
|
16
17
|
- [Usage](#usage)
|
17
18
|
- [Setup](#setup)
|
18
|
-
|
19
|
+
- [Generic](#generic)
|
20
|
+
- [for_gem](#for_gem)
|
21
|
+
- [Autoloading](#autoloading)
|
19
22
|
- [Eager loading](#eager-loading)
|
23
|
+
- [Reloading](#reloading)
|
20
24
|
- [Inflection](#inflection)
|
21
25
|
- [Zeitwerk::Inflector](#zeitwerkinflector)
|
22
26
|
- [Zeitwerk::GemInflector](#zeitwerkgeminflector)
|
@@ -28,10 +32,14 @@
|
|
28
32
|
- [Use case: The adapter pattern](#use-case-the-adapter-pattern)
|
29
33
|
- [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
|
30
34
|
- [Edge cases](#edge-cases)
|
35
|
+
- [Reopening third-party namespaces](#reopening-third-party-namespaces)
|
31
36
|
- [Rules of thumb](#rules-of-thumb)
|
32
|
-
- [
|
37
|
+
- [Debuggers](#debuggers)
|
38
|
+
- [Break](#break)
|
39
|
+
- [Byebug](#byebug)
|
33
40
|
- [Pronunciation](#pronunciation)
|
34
41
|
- [Supported Ruby versions](#supported-ruby-versions)
|
42
|
+
- [Testing](#testing)
|
35
43
|
- [Motivation](#motivation)
|
36
44
|
- [Thanks](#thanks)
|
37
45
|
- [License](#license)
|
@@ -49,7 +57,9 @@ Zeitwerk is also able to reload code, which may be handy while developing web ap
|
|
49
57
|
|
50
58
|
The gem is designed so that any project, gem dependency, application, etc. can have their own independent loader, coexisting in the same process, managing their own project trees, and independent of each other. Each loader has its own configuration, inflector, and optional logger.
|
51
59
|
|
52
|
-
Internally, Zeitwerk issues `require` calls exclusively using absolute file names, so there are no costly file system lookups in `$LOAD_PATH`. Technically, the directories managed by Zeitwerk do not even need to be in `$LOAD_PATH`.
|
60
|
+
Internally, Zeitwerk issues `require` calls exclusively using absolute file names, so there are no costly file system lookups in `$LOAD_PATH`. Technically, the directories managed by Zeitwerk do not even need to be in `$LOAD_PATH`.
|
61
|
+
|
62
|
+
Furthermore, Zeitwerk does at most one single scan of the project tree, and it descends into subdirectories lazily, only if their namespaces are used.
|
53
63
|
|
54
64
|
<a id="markdown-synopsis" name="synopsis"></a>
|
55
65
|
## Synopsis
|
@@ -129,6 +139,22 @@ app/models/user.rb -> User
|
|
129
139
|
app/controllers/admin/users_controller.rb -> Admin::UsersController
|
130
140
|
```
|
131
141
|
|
142
|
+
Alternatively, you can associate a custom namespace to a root directory by passing a class or module object in the optional `namespace` keyword argument.
|
143
|
+
|
144
|
+
For example, Active Job queue adapters have to define a constant after their name in `ActiveJob::QueueAdapters`.
|
145
|
+
|
146
|
+
So, if you declare
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
require "active_job"
|
150
|
+
require "active_job/queue_adapters"
|
151
|
+
loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
|
152
|
+
```
|
153
|
+
|
154
|
+
your adapter can be stored directly in that directory instead of the canonical `lib/active_job/queue_adapters`.
|
155
|
+
|
156
|
+
Please, note that the given namespace must be non-reloadable, though autoloaded constants in that namespace can be. That is, if you associate `app/api` with an existing `Api` module, that module should not be reloadable. However, if the project defines and autoloads the class `Api::V2::Deliveries`, that one can be reloaded.
|
157
|
+
|
132
158
|
<a id="markdown-implicit-namespaces" name="implicit-namespaces"></a>
|
133
159
|
### Implicit namespaces
|
134
160
|
|
@@ -163,6 +189,32 @@ end
|
|
163
189
|
|
164
190
|
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.
|
165
191
|
|
192
|
+
<a id="markdown-collapsing-directories" name="collapsing-directories"></a>
|
193
|
+
### Collapsing directories
|
194
|
+
|
195
|
+
Say some directories in a project exist for organizational purposes only, and you prefer not to have them as namespaces. For example, the `actions` subdirectory in the next example is not meant to represent a namespace, it is there only to group all actions related to bookings:
|
196
|
+
|
197
|
+
```
|
198
|
+
booking.rb -> Booking
|
199
|
+
booking/actions/create.rb -> Booking::Create
|
200
|
+
```
|
201
|
+
|
202
|
+
To make it work that way, configure Zeitwerk to collapse said directory:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
loader.collapse("booking/actions")
|
206
|
+
```
|
207
|
+
|
208
|
+
This method accepts an arbitrary number of strings or `Pathname` objects, and also an array of them.
|
209
|
+
|
210
|
+
You can pass directories and glob patterns. Glob patterns are expanded when they are added, and again on each reload.
|
211
|
+
|
212
|
+
To illustrate usage of glob patterns, if `actions` in the example above is part of a standardized structure, you could use a wildcard:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
loader.collapse("*/actions")
|
216
|
+
```
|
217
|
+
|
166
218
|
<a id="markdown-nested-root-directories" name="nested-root-directories"></a>
|
167
219
|
### Nested root directories
|
168
220
|
|
@@ -182,6 +234,9 @@ should define `Geolocatable`, not `Concerns::Geolocatable`.
|
|
182
234
|
<a id="markdown-setup" name="setup"></a>
|
183
235
|
### Setup
|
184
236
|
|
237
|
+
<a id="markdown-generic" name="generic"></a>
|
238
|
+
#### Generic
|
239
|
+
|
185
240
|
Loaders are ready to load code right after calling `setup` on them:
|
186
241
|
|
187
242
|
```ruby
|
@@ -198,9 +253,36 @@ loader.push_dir(...)
|
|
198
253
|
loader.setup
|
199
254
|
```
|
200
255
|
|
201
|
-
|
256
|
+
<a id="markdown-for_gem" name="for_gem"></a>
|
257
|
+
#### for_gem
|
258
|
+
|
259
|
+
`Zeitwerk::Loader.for_gem` is a convenience shortcut for the common case in which a gem has its entry point directly under the `lib` directory:
|
260
|
+
|
261
|
+
```
|
262
|
+
lib/my_gem.rb # MyGem
|
263
|
+
lib/my_gem/version.rb # MyGem::VERSION
|
264
|
+
lib/my_gem/foo.rb # MyGem::Foo
|
265
|
+
```
|
266
|
+
|
267
|
+
Neither a gemspec nor a version file are technically required, this helper works as long as the code is organized using that standard structure.
|
202
268
|
|
203
|
-
If the
|
269
|
+
If the entry point of your gem lives in a subdirectory of `lib` because it is reopening a namespace defined somewhere else, please use the generic API to setup the loader, and make sure you check the section [_Reopening third-party namespaces_](https://github.com/fxn/zeitwerk#reopening-third-party-namespaces) down below.
|
270
|
+
|
271
|
+
Conceptually, `for_gem` translates to:
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
# lib/my_gem.rb
|
275
|
+
|
276
|
+
require "zeitwerk"
|
277
|
+
loader = Zeitwerk::Loader.new
|
278
|
+
loader.tag = File.basename(__FILE__, ".rb")
|
279
|
+
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
280
|
+
loader.push_dir(__dir__)
|
281
|
+
```
|
282
|
+
|
283
|
+
except that this method returns the same object in subsequent calls from the same file, in the unlikely case the gem wants to be able to reload.
|
284
|
+
|
285
|
+
If the main module references project constants at the top-level, Zeitwerk has to be ready to load them. Their definitions, in turn, may reference other project constants. And this is recursive. Therefore, it is important that the `setup` call happens above the main module definition:
|
204
286
|
|
205
287
|
```ruby
|
206
288
|
# lib/my_gem.rb (main file)
|
@@ -216,35 +298,28 @@ module MyGem
|
|
216
298
|
end
|
217
299
|
```
|
218
300
|
|
219
|
-
|
301
|
+
<a id="markdown-autoloading" name="autoloading"></a>
|
302
|
+
### Autoloading
|
220
303
|
|
221
|
-
|
222
|
-
### Reloading
|
304
|
+
After `setup`, you are able to reference classes and modules from the project without issuing `require` calls for them. They are all available everywhere, autoloading loads them on demand. This works even if the reference to the class or module is first hit in client code, outside your project.
|
223
305
|
|
224
|
-
|
306
|
+
Let's revisit the example above:
|
225
307
|
|
226
308
|
```ruby
|
227
|
-
|
228
|
-
loader.push_dir(...)
|
229
|
-
loader.enable_reloading # you need to opt-in before setup
|
230
|
-
loader.setup
|
231
|
-
...
|
232
|
-
loader.reload
|
233
|
-
```
|
234
|
-
|
235
|
-
There is no way to undo this, either you want to reload or you don't.
|
236
|
-
|
237
|
-
Enabling reloading after setup raises `Zeitwerk::Error`. Similarly, calling `reload` without having enabled reloading also raises `Zeitwerk::Error`.
|
309
|
+
# lib/my_gem.rb (main file)
|
238
310
|
|
239
|
-
|
311
|
+
require "zeitwerk"
|
312
|
+
loader = Zeitwerk::Loader.for_gem
|
313
|
+
loader.setup
|
240
314
|
|
241
|
-
|
315
|
+
module MyGem
|
316
|
+
include MyLogger # (*)
|
317
|
+
end
|
318
|
+
```
|
242
319
|
|
243
|
-
|
320
|
+
That works, and there is no `require "my_gem/my_logger"`. When `(*)` is reached, Zeitwerk seamlessly autoloads `MyGem::MyLogger`.
|
244
321
|
|
245
|
-
|
246
|
-
|
247
|
-
On reloading, client code has to update anything that would otherwise be storing a stale object. For example, if the routing layer of a web framework stores controller class objects or instances in internal structures, on reload it has to refresh them somehow, possibly reevaluating routes.
|
322
|
+
If autoloading a file does not define the expected class or module, Zeitwerk raises `Zeitwerk::NameError`, which is a subclass of `NameError`.
|
248
323
|
|
249
324
|
<a id="markdown-eager-loading" name="eager-loading"></a>
|
250
325
|
### Eager loading
|
@@ -268,6 +343,8 @@ In gems, the method needs to be invoked after the main namespace has been define
|
|
268
343
|
|
269
344
|
Eager loading is synchronized and idempotent.
|
270
345
|
|
346
|
+
If eager loading a file does not define the expected class or module, Zeitwerk raises `Zeitwerk::NameError`, which is a subclass of `NameError`.
|
347
|
+
|
271
348
|
If you want to eager load yourself and all dependencies using Zeitwerk, you can broadcast the `eager_load` call to all instances:
|
272
349
|
|
273
350
|
```ruby
|
@@ -278,6 +355,34 @@ This may be handy in top-level services, like web applications.
|
|
278
355
|
|
279
356
|
Note that thanks to idempotence `Zeitwerk::Loader.eager_load_all` won't eager load twice if any of the instances already eager loaded.
|
280
357
|
|
358
|
+
<a id="markdown-reloading" name="reloading"></a>
|
359
|
+
### Reloading
|
360
|
+
|
361
|
+
Zeitwerk is able to reload code, but you need to enable this feature:
|
362
|
+
|
363
|
+
```ruby
|
364
|
+
loader = Zeitwerk::Loader.new
|
365
|
+
loader.push_dir(...)
|
366
|
+
loader.enable_reloading # you need to opt-in before setup
|
367
|
+
loader.setup
|
368
|
+
...
|
369
|
+
loader.reload
|
370
|
+
```
|
371
|
+
|
372
|
+
There is no way to undo this, either you want to reload or you don't.
|
373
|
+
|
374
|
+
Enabling reloading after setup raises `Zeitwerk::Error`. Attempting to reload without having it enabled raises `Zeitwerk::ReloadingDisabledError`.
|
375
|
+
|
376
|
+
Generally speaking, reloading is useful while developing running services like web applications. Gems that implement regular libraries, so to speak, or services running in testing or production environments, won't normally have a use case for reloading. If reloading is not enabled, Zeitwerk is able to use less memory.
|
377
|
+
|
378
|
+
Reloading removes the currently loaded classes and modules and resets the loader so that it will pick whatever is in the file system now.
|
379
|
+
|
380
|
+
It is important to highlight that this is an instance method. Don't worry about project dependencies managed by Zeitwerk, their loaders are independent.
|
381
|
+
|
382
|
+
In order for reloading to be thread-safe, you need to implement some coordination. For example, a web framework that serves each request with its own thread may have a globally accessible RW lock. When a request comes in, the framework acquires the lock for reading at the beginning, and the code in the framework that calls `loader.reload` needs to acquire the lock for writing.
|
383
|
+
|
384
|
+
On reloading, client code has to update anything that would otherwise be storing a stale object. For example, if the routing layer of a web framework stores controller class objects or instances in internal structures, on reload it has to refresh them somehow, possibly reevaluating routes.
|
385
|
+
|
281
386
|
<a id="markdown-inflection" name="inflection"></a>
|
282
387
|
### Inflection
|
283
388
|
|
@@ -565,6 +670,42 @@ Trip = Struct.new { ... } # NOT SUPPORTED
|
|
565
670
|
|
566
671
|
This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
|
567
672
|
|
673
|
+
<a id="markdown-reopening-third-party-namespaces" name="reopening-third-party-namespaces"></a>
|
674
|
+
### Reopening third-party namespaces
|
675
|
+
|
676
|
+
Projects managed by Zeitwerk can work with namespaces defined by third-party libraries. However, they have to be loaded in memory before calling `setup`.
|
677
|
+
|
678
|
+
For example, let's imagine you're writing a gem that implements an adapter for [Active Job](https://guides.rubyonrails.org/active_job_basics.html) that uses AwesomeQueue as backend. By convention, your gem has to define a class called `ActiveJob::QueueAdapters::AwesomeQueue`, and it has to do so in a file with a matching path:
|
679
|
+
|
680
|
+
```ruby
|
681
|
+
# lib/active_job/queue_adapters/awesome_queue.rb
|
682
|
+
module ActiveJob
|
683
|
+
module QueueAdapters
|
684
|
+
class AwesomeQueue
|
685
|
+
# ...
|
686
|
+
end
|
687
|
+
end
|
688
|
+
end
|
689
|
+
```
|
690
|
+
|
691
|
+
It is very important that your gem _reopens_ the modules `ActiveJob` and `ActiveJob::QueueAdapters` instead of _defining_ them. Because their proper definition lives in Active Job. Furthermore, if the project reloads, you do not want any of `ActiveJob` or `ActiveJob::QueueAdapters` to be reloaded.
|
692
|
+
|
693
|
+
Bottom line, Zeitwerk should not be managing those namespaces. Active Job owns them and defines them. Your gem needs to _reopen_ them.
|
694
|
+
|
695
|
+
In order to do so, you need to make sure those modules are loaded before calling `setup`. For instance, in the entry file for the gem:
|
696
|
+
|
697
|
+
```ruby
|
698
|
+
# Ensure these namespaces are reopened, not defined.
|
699
|
+
require "active_job"
|
700
|
+
require "active_job/queue_adapters"
|
701
|
+
|
702
|
+
require "zeitwerk"
|
703
|
+
loader = Zeitwerk::Loader.for_gem
|
704
|
+
loader.setup
|
705
|
+
```
|
706
|
+
|
707
|
+
With that, when Zeitwerk scans the file system and reaches the gem directories `lib/active_job` and `lib/active_job/queue_adapters`, it detects the corresponding modules already exist and therefore understands it does not have to manage them. The loader just descends into those directories. Eventually will reach `lib/active_job/queue_adapters/awesome_queue.rb`, and since `ActiveJob::QueueAdapters::AwesomeQueue` is unknown, Zeitwerk will manage it. Which is what happens regularly with the files in your gem. On reload, the namespaces are safe, won't be reloaded. The loader only reloads what it manages, which in this case is the adapter itself.
|
708
|
+
|
568
709
|
<a id="markdown-rules-of-thumb" name="rules-of-thumb"></a>
|
569
710
|
### Rules of thumb
|
570
711
|
|
@@ -580,14 +721,18 @@ This only affects explicit namespaces, those idioms work well for any other ordi
|
|
580
721
|
|
581
722
|
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.
|
582
723
|
|
583
|
-
<a id="markdown-
|
584
|
-
###
|
724
|
+
<a id="markdown-debuggers" name="debuggers"></a>
|
725
|
+
### Debuggers
|
726
|
+
|
727
|
+
<a id="markdown-break" name="break"></a>
|
728
|
+
#### Break
|
585
729
|
|
586
|
-
|
730
|
+
Zeitwerk works fine with [@gsamokovarov](https://github.com/gsamokovarov)'s [Break](https://github.com/gsamokovarov/break) debugger.
|
587
731
|
|
588
|
-
|
732
|
+
<a id="markdown-byebug" name="byebug"></a>
|
733
|
+
#### Byebug
|
589
734
|
|
590
|
-
|
735
|
+
Zeitwerk and [Byebug](https://github.com/deivid-rodriguez/byebug) are incompatible, classes or modules that belong to [explicit namespaces](#explicit-namespaces) are not autoloaded inside a Byebug session. See [this issue](https://github.com/deivid-rodriguez/byebug/issues/564#issuecomment-499413606) for further details.
|
591
736
|
|
592
737
|
<a id="markdown-pronunciation" name="pronunciation"></a>
|
593
738
|
## Pronunciation
|
@@ -599,6 +744,32 @@ As a workaround, you can eager load. Zeitwerk tries hard to succeed or fail cons
|
|
599
744
|
|
600
745
|
Zeitwerk works with MRI 2.4.4 and above.
|
601
746
|
|
747
|
+
<a id="markdown-testing" name="testing"></a>
|
748
|
+
## Testing
|
749
|
+
|
750
|
+
In order to run the test suite of Zeitwerk, `cd` into the project root and execute
|
751
|
+
|
752
|
+
```
|
753
|
+
bin/test
|
754
|
+
```
|
755
|
+
|
756
|
+
To run one particular suite, pass its file name as an argument:
|
757
|
+
|
758
|
+
```
|
759
|
+
bin/test test/lib/zeitwerk/test_eager_load.rb
|
760
|
+
```
|
761
|
+
|
762
|
+
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`:
|
763
|
+
|
764
|
+
```ruby
|
765
|
+
focus
|
766
|
+
test "capitalizes the first letter" do
|
767
|
+
assert_equal "User", camelize("user")
|
768
|
+
end
|
769
|
+
```
|
770
|
+
|
771
|
+
and run `bin/test`.
|
772
|
+
|
602
773
|
<a id="markdown-motivation" name="motivation"></a>
|
603
774
|
## Motivation
|
604
775
|
|
@@ -613,6 +784,8 @@ I'd like to thank [@matthewd](https://github.com/matthewd) for the discussions w
|
|
613
784
|
|
614
785
|
Also, would like to thank [@Shopify](https://github.com/Shopify), [@rafaelfranca](https://github.com/rafaelfranca), and [@dylanahsmith](https://github.com/dylanahsmith), for sharing [this PoC](https://github.com/Shopify/autoload_reloader). The technique Zeitwerk uses to support explicit namespaces was copied from that project.
|
615
786
|
|
787
|
+
Jean Boussier ([@casperisfine](https://github.com/casperisfine), [@byroot](https://github.com/byroot)) deserves special mention. Jean migrated autoloading in Shopify when Zeitwerk integration in Rails was yet unreleased. His work and positive attitude have been outstanding, and thanks to his feedback the interface and performance of Zeitwerk are way, way better. Kudos man ❤️.
|
788
|
+
|
616
789
|
Finally, many thanks to [@schurig](https://github.com/schurig) for recording an [audio file](http://share.hashref.com/zeitwerk/zeitwerk_pronunciation.mp3) with the pronunciation of "Zeitwerk" in perfect German. 💯
|
617
790
|
|
618
791
|
<a id="markdown-license" name="license"></a>
|
data/lib/zeitwerk/inflector.rb
CHANGED
@@ -15,7 +15,7 @@ module Zeitwerk
|
|
15
15
|
# @param _abspath [String]
|
16
16
|
# @return [String]
|
17
17
|
def camelize(basename, _abspath)
|
18
|
-
overrides[basename] || basename.split('_').
|
18
|
+
overrides[basename] || basename.split('_').each(&:capitalize!).join
|
19
19
|
end
|
20
20
|
|
21
21
|
# Configures hard-coded inflections:
|
@@ -28,7 +28,7 @@ module Zeitwerk
|
|
28
28
|
#
|
29
29
|
# inflector.camelize("html_parser", abspath) # => "HTMLParser"
|
30
30
|
# inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
|
31
|
-
# inflector.camelize("users_controller", abspath) # => "
|
31
|
+
# inflector.camelize("users_controller", abspath) # => "UsersController"
|
32
32
|
#
|
33
33
|
# @param inflections [{String => String}]
|
34
34
|
# @return [void]
|
data/lib/zeitwerk/kernel.rb
CHANGED
@@ -3,6 +3,17 @@
|
|
3
3
|
module Kernel
|
4
4
|
module_function
|
5
5
|
|
6
|
+
# We are going to decorate Kerner#require with two goals.
|
7
|
+
#
|
8
|
+
# First, by intercepting Kernel#require calls, we are able to autovivify
|
9
|
+
# modules on required directories, and also do internal housekeeping when
|
10
|
+
# managed files are loaded.
|
11
|
+
#
|
12
|
+
# On the other hand, if you publish a new version of a gem that is now managed
|
13
|
+
# by Zeitwerk, client code can reference directly your classes and modules and
|
14
|
+
# should not require anything. But if someone has legacy require calls around,
|
15
|
+
# they will work as expected, and in a compatible way.
|
16
|
+
#
|
6
17
|
# We cannot decorate with prepend + super because Kernel has already been
|
7
18
|
# included in Object, and changes in ancestors don't get propagated into
|
8
19
|
# already existing ancestor chains.
|
@@ -30,4 +41,24 @@ module Kernel
|
|
30
41
|
end
|
31
42
|
end
|
32
43
|
end
|
44
|
+
|
45
|
+
# By now, I have seen no way so far to decorate require_relative.
|
46
|
+
#
|
47
|
+
# For starters, at least in CRuby, require_relative does not delegate to
|
48
|
+
# require. Both require and require_relative delegate the bulk of their work
|
49
|
+
# to an internal C function called rb_require_safe. So, our require wrapper is
|
50
|
+
# not executed.
|
51
|
+
#
|
52
|
+
# On the other hand, we cannot use the aliasing technique above because
|
53
|
+
# require_relative receives a path relative to the directory of the file in
|
54
|
+
# which the call is performed. If a wrapper here invoked the original method,
|
55
|
+
# Ruby would resolve the relative path taking lib/zeitwerk as base directory.
|
56
|
+
#
|
57
|
+
# A workaround could be to extract the base directory from caller_locations,
|
58
|
+
# but what if someone else decorated require_relative before us? You can't
|
59
|
+
# really know with certainty where's the original call site in the stack.
|
60
|
+
#
|
61
|
+
# However, the main use case for require_relative is to load files from your
|
62
|
+
# own project. Projects managed by Zeitwerk don't do this for files managed by
|
63
|
+
# Zeitwerk, precisely.
|
33
64
|
end
|
data/lib/zeitwerk/loader.rb
CHANGED
@@ -39,7 +39,7 @@ module Zeitwerk
|
|
39
39
|
# @return [<String>]
|
40
40
|
attr_reader :preloads
|
41
41
|
|
42
|
-
# Absolute paths of files, directories,
|
42
|
+
# Absolute paths of files, directories, or glob patterns to be totally
|
43
43
|
# ignored.
|
44
44
|
#
|
45
45
|
# @private
|
@@ -54,6 +54,19 @@ module Zeitwerk
|
|
54
54
|
# @return [Set<String>]
|
55
55
|
attr_reader :ignored_paths
|
56
56
|
|
57
|
+
# Absolute paths of directories or glob patterns to be collapsed.
|
58
|
+
#
|
59
|
+
# @private
|
60
|
+
# @return [Set<String>]
|
61
|
+
attr_reader :collapse_glob_patterns
|
62
|
+
|
63
|
+
# The actual collection of absolute directory names at the time the collapse
|
64
|
+
# glob patterns were expanded. Computed on setup, and recomputed on reload.
|
65
|
+
#
|
66
|
+
# @private
|
67
|
+
# @return [Set<String>]
|
68
|
+
attr_reader :collapse_dirs
|
69
|
+
|
57
70
|
# Maps real absolute paths for which an autoload has been set ---and not
|
58
71
|
# executed--- to their corresponding parent class or module and constant
|
59
72
|
# name.
|
@@ -131,15 +144,17 @@ module Zeitwerk
|
|
131
144
|
@inflector = Inflector.new
|
132
145
|
@logger = self.class.default_logger
|
133
146
|
|
134
|
-
@root_dirs
|
135
|
-
@preloads
|
136
|
-
@ignored_glob_patterns
|
137
|
-
@ignored_paths
|
138
|
-
@
|
139
|
-
@
|
140
|
-
@
|
141
|
-
@
|
142
|
-
@
|
147
|
+
@root_dirs = {}
|
148
|
+
@preloads = []
|
149
|
+
@ignored_glob_patterns = Set.new
|
150
|
+
@ignored_paths = Set.new
|
151
|
+
@collapse_glob_patterns = Set.new
|
152
|
+
@collapse_dirs = Set.new
|
153
|
+
@autoloads = {}
|
154
|
+
@autoloaded_dirs = []
|
155
|
+
@to_unload = {}
|
156
|
+
@lazy_subdirs = {}
|
157
|
+
@eager_load_exclusions = Set.new
|
143
158
|
|
144
159
|
# TODO: find a better name for these mutexes.
|
145
160
|
@mutex = Mutex.new
|
@@ -154,6 +169,7 @@ module Zeitwerk
|
|
154
169
|
|
155
170
|
# Sets a tag for the loader, useful for logging.
|
156
171
|
#
|
172
|
+
# @param tag [#to_s]
|
157
173
|
# @return [void]
|
158
174
|
def tag=(tag)
|
159
175
|
@tag = tag.to_s
|
@@ -174,13 +190,19 @@ module Zeitwerk
|
|
174
190
|
# or descendants.
|
175
191
|
#
|
176
192
|
# @param path [<String, Pathname>]
|
193
|
+
# @param namespace [Class, Module]
|
177
194
|
# @raise [Zeitwerk::Error]
|
178
195
|
# @return [void]
|
179
|
-
def push_dir(path)
|
196
|
+
def push_dir(path, namespace: Object)
|
197
|
+
# Note that Class < Module.
|
198
|
+
unless namespace.is_a?(Module)
|
199
|
+
raise Error, "#{namespace.inspect} is not a class or module object, should be"
|
200
|
+
end
|
201
|
+
|
180
202
|
abspath = File.expand_path(path)
|
181
203
|
if dir?(abspath)
|
182
204
|
raise_if_conflicting_directory(abspath)
|
183
|
-
root_dirs[abspath] =
|
205
|
+
root_dirs[abspath] = namespace
|
184
206
|
else
|
185
207
|
raise Error, "the root directory #{abspath} does not exist"
|
186
208
|
end
|
@@ -233,6 +255,18 @@ module Zeitwerk
|
|
233
255
|
end
|
234
256
|
end
|
235
257
|
|
258
|
+
# Configure directories or glob patterns to be collapsed.
|
259
|
+
#
|
260
|
+
# @param paths [<String, Pathname, <String, Pathname>>]
|
261
|
+
# @return [void]
|
262
|
+
def collapse(*glob_patterns)
|
263
|
+
glob_patterns = expand_paths(glob_patterns)
|
264
|
+
mutex.synchronize do
|
265
|
+
collapse_glob_patterns.merge(glob_patterns)
|
266
|
+
collapse_dirs.merge(expand_glob_patterns(glob_patterns))
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
236
270
|
# Sets autoloads in the root namespace and preloads files, if any.
|
237
271
|
#
|
238
272
|
# @return [void]
|
@@ -240,7 +274,9 @@ module Zeitwerk
|
|
240
274
|
mutex.synchronize do
|
241
275
|
break if @setup
|
242
276
|
|
243
|
-
actual_root_dirs.each
|
277
|
+
actual_root_dirs.each do |root_dir, namespace|
|
278
|
+
set_autoloads_in_dir(root_dir, namespace)
|
279
|
+
end
|
244
280
|
do_preload
|
245
281
|
|
246
282
|
@setup = true
|
@@ -306,7 +342,8 @@ module Zeitwerk
|
|
306
342
|
Registry.on_unload(self)
|
307
343
|
ExplicitNamespace.unregister(self)
|
308
344
|
|
309
|
-
@setup
|
345
|
+
@setup = false
|
346
|
+
@eager_loaded = false
|
310
347
|
end
|
311
348
|
end
|
312
349
|
|
@@ -322,6 +359,7 @@ module Zeitwerk
|
|
322
359
|
if reloading_enabled?
|
323
360
|
unload
|
324
361
|
recompute_ignored_paths
|
362
|
+
recompute_collapse_dirs
|
325
363
|
setup
|
326
364
|
else
|
327
365
|
raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
|
@@ -338,8 +376,11 @@ module Zeitwerk
|
|
338
376
|
mutex.synchronize do
|
339
377
|
break if @eager_loaded
|
340
378
|
|
341
|
-
queue =
|
342
|
-
|
379
|
+
queue = []
|
380
|
+
actual_root_dirs.each do |root_dir, namespace|
|
381
|
+
queue << [namespace, root_dir] unless eager_load_exclusions.member?(root_dir)
|
382
|
+
end
|
383
|
+
|
343
384
|
while to_eager_load = queue.shift
|
344
385
|
namespace, dir = to_eager_load
|
345
386
|
|
@@ -351,8 +392,12 @@ module Zeitwerk
|
|
351
392
|
cref[0].const_get(cref[1], false)
|
352
393
|
end
|
353
394
|
elsif dir?(abspath) && !root_dirs.key?(abspath)
|
354
|
-
|
355
|
-
|
395
|
+
if collapse_dirs.member?(abspath)
|
396
|
+
queue << [namespace, abspath]
|
397
|
+
else
|
398
|
+
cname = inflector.camelize(basename, abspath)
|
399
|
+
queue << [namespace.const_get(cname, false), abspath]
|
400
|
+
end
|
356
401
|
end
|
357
402
|
end
|
358
403
|
end
|
@@ -430,7 +475,7 @@ module Zeitwerk
|
|
430
475
|
# require "zeitwerk"
|
431
476
|
# loader = Zeitwerk::Loader.new
|
432
477
|
# loader.tag = File.basename(__FILE__, ".rb")
|
433
|
-
# loader.inflector = Zeitwerk::GemInflector.new
|
478
|
+
# loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
434
479
|
# loader.push_dir(__dir__)
|
435
480
|
#
|
436
481
|
# except that this method returns the same object in subsequent calls from
|
@@ -464,7 +509,7 @@ module Zeitwerk
|
|
464
509
|
|
465
510
|
# @return [<String>]
|
466
511
|
def actual_root_dirs
|
467
|
-
root_dirs.
|
512
|
+
root_dirs.reject do |root_dir, _namespace|
|
468
513
|
!dir?(root_dir) || ignored_paths.member?(root_dir)
|
469
514
|
end
|
470
515
|
end
|
@@ -476,7 +521,7 @@ module Zeitwerk
|
|
476
521
|
ls(dir) do |basename, abspath|
|
477
522
|
begin
|
478
523
|
if ruby?(basename)
|
479
|
-
basename
|
524
|
+
basename[-3..-1] = ''
|
480
525
|
cname = inflector.camelize(basename, abspath).to_sym
|
481
526
|
autoload_file(parent, cname, abspath)
|
482
527
|
elsif dir?(abspath)
|
@@ -488,13 +533,17 @@ module Zeitwerk
|
|
488
533
|
# it counts only as root. The guard checks that.
|
489
534
|
unless root_dirs.key?(abspath)
|
490
535
|
cname = inflector.camelize(basename, abspath).to_sym
|
491
|
-
|
536
|
+
if collapse_dirs.member?(abspath)
|
537
|
+
set_autoloads_in_dir(abspath, parent)
|
538
|
+
else
|
539
|
+
autoload_subdir(parent, cname, abspath)
|
540
|
+
end
|
492
541
|
end
|
493
542
|
end
|
494
543
|
rescue ::NameError => error
|
495
544
|
path_type = ruby?(abspath) ? "file" : "directory"
|
496
545
|
|
497
|
-
raise NameError,
|
546
|
+
raise NameError.new(<<~MESSAGE, error.name)
|
498
547
|
#{error.message} inferred by #{inflector.class} from #{path_type}
|
499
548
|
|
500
549
|
#{abspath}
|
@@ -558,7 +607,7 @@ module Zeitwerk
|
|
558
607
|
end
|
559
608
|
|
560
609
|
# @param dir [String] directory that would have autovivified a module
|
561
|
-
# @param file [String] the file where the namespace is
|
610
|
+
# @param file [String] the file where the namespace is explicitly defined
|
562
611
|
# @param parent [Module]
|
563
612
|
# @param cname [Symbol]
|
564
613
|
# @return [void]
|
@@ -578,7 +627,10 @@ module Zeitwerk
|
|
578
627
|
# $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the
|
579
628
|
# real path to be able to delete it from $LOADED_FEATURES on unload, and to
|
580
629
|
# be able to do a lookup later in Kernel#require for manual require calls.
|
581
|
-
|
630
|
+
#
|
631
|
+
# We freeze realpath because that saves allocations in Module#autoload.
|
632
|
+
# See #125.
|
633
|
+
realpath = File.realpath(abspath).freeze
|
582
634
|
parent.autoload(cname, realpath)
|
583
635
|
if logger
|
584
636
|
if ruby?(realpath)
|
@@ -623,8 +675,14 @@ module Zeitwerk
|
|
623
675
|
# @param parent [Module]
|
624
676
|
# @param cname [Symbol]
|
625
677
|
# @return [String, nil]
|
626
|
-
|
627
|
-
|
678
|
+
if method(:autoload?).arity == 1
|
679
|
+
def strict_autoload_path(parent, cname)
|
680
|
+
parent.autoload?(cname) if cdef?(parent, cname)
|
681
|
+
end
|
682
|
+
else
|
683
|
+
def strict_autoload_path(parent, cname)
|
684
|
+
parent.autoload?(cname, false)
|
685
|
+
end
|
628
686
|
end
|
629
687
|
|
630
688
|
# This method is called this way because I prefer `preload` to be the method
|
@@ -675,8 +733,13 @@ module Zeitwerk
|
|
675
733
|
def ls(dir)
|
676
734
|
Dir.foreach(dir) do |basename|
|
677
735
|
next if basename.start_with?(".")
|
736
|
+
|
678
737
|
abspath = File.join(dir, basename)
|
679
|
-
|
738
|
+
next if ignored_paths.member?(abspath)
|
739
|
+
|
740
|
+
# We freeze abspath because that saves allocations when passed later to
|
741
|
+
# File methods. See #125.
|
742
|
+
yield basename, abspath.freeze
|
680
743
|
end
|
681
744
|
end
|
682
745
|
|
@@ -711,6 +774,11 @@ module Zeitwerk
|
|
711
774
|
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
712
775
|
end
|
713
776
|
|
777
|
+
# @return [void]
|
778
|
+
def recompute_collapse_dirs
|
779
|
+
collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
|
780
|
+
end
|
781
|
+
|
714
782
|
# @param message [String]
|
715
783
|
# @return [void]
|
716
784
|
def log(message)
|
@@ -14,7 +14,7 @@ module Zeitwerk::Loader::Callbacks
|
|
14
14
|
if logger && cdef?(*cref)
|
15
15
|
log("constant #{cpath(*cref)} loaded from file #{file}")
|
16
16
|
elsif !cdef?(*cref)
|
17
|
-
raise NameError
|
17
|
+
raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath(*cref)}, but didn't", cref.last)
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
@@ -9,7 +9,13 @@ module Zeitwerk::RealModName
|
|
9
9
|
#
|
10
10
|
# @param mod [Class, Module]
|
11
11
|
# @return [String, nil]
|
12
|
-
|
13
|
-
|
12
|
+
if UnboundMethod.method_defined?(:bind_call)
|
13
|
+
def real_mod_name(mod)
|
14
|
+
UNBOUND_METHOD_MODULE_NAME.bind_call(mod)
|
15
|
+
end
|
16
|
+
else
|
17
|
+
def real_mod_name(mod)
|
18
|
+
UNBOUND_METHOD_MODULE_NAME.bind(mod).call
|
19
|
+
end
|
14
20
|
end
|
15
21
|
end
|
data/lib/zeitwerk/version.rb
CHANGED
metadata
CHANGED
@@ -1,19 +1,19 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zeitwerk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Xavier Noria
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-07-14 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |2
|
14
14
|
Zeitwerk implements constant autoloading with Ruby semantics. Each gem
|
15
15
|
and application may have their own independent autoloader, with its own
|
16
|
-
configuration, inflector, and logger. Supports autoloading,
|
16
|
+
configuration, inflector, and logger. Supports autoloading,
|
17
17
|
reloading, and eager loading.
|
18
18
|
email: fxn@hashref.com
|
19
19
|
executables: []
|
@@ -36,7 +36,11 @@ files:
|
|
36
36
|
homepage: https://github.com/fxn/zeitwerk
|
37
37
|
licenses:
|
38
38
|
- MIT
|
39
|
-
metadata:
|
39
|
+
metadata:
|
40
|
+
homepage_uri: https://github.com/fxn/zeitwerk
|
41
|
+
changelog_uri: https://github.com/fxn/zeitwerk/blob/master/CHANGELOG.md
|
42
|
+
source_code_uri: https://github.com/fxn/zeitwerk
|
43
|
+
bug_tracker_uri: https://github.com/fxn/zeitwerk/issues
|
40
44
|
post_install_message:
|
41
45
|
rdoc_options: []
|
42
46
|
require_paths:
|
@@ -52,7 +56,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
52
56
|
- !ruby/object:Gem::Version
|
53
57
|
version: '0'
|
54
58
|
requirements: []
|
55
|
-
rubygems_version: 3.
|
59
|
+
rubygems_version: 3.1.2
|
56
60
|
signing_key:
|
57
61
|
specification_version: 4
|
58
62
|
summary: Efficient and thread-safe constant autoloader
|