zeitwerk 2.4.2 → 2.5.0.beta
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +163 -60
- data/lib/zeitwerk.rb +2 -0
- data/lib/zeitwerk/autoloads.rb +69 -0
- data/lib/zeitwerk/explicit_namespace.rb +8 -2
- data/lib/zeitwerk/kernel.rb +5 -4
- data/lib/zeitwerk/loader.rb +75 -412
- data/lib/zeitwerk/loader/callbacks.rb +9 -8
- data/lib/zeitwerk/loader/config.rb +308 -0
- data/lib/zeitwerk/loader/helpers.rb +95 -0
- data/lib/zeitwerk/registry.rb +7 -7
- data/lib/zeitwerk/version.rb +1 -1
- metadata +9 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7ac5e0ce2eba71a2099ead24de8762e3e737656ff4bcae57c83bb5797e922f72
|
4
|
+
data.tar.gz: 85e7c762a5164301ba5bcce82a31e6c49bc7c6caff2b4abff23c425f10374c69
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f258acabd660f54702eb904550a109b08bdacff2cecc92d610376376f77ccf70ed421e09c64144930d2a145ebbedb7e9c3c26ff21ad2c1b40dd0537d9947b4d4
|
7
|
+
data.tar.gz: 1612dc2d505fe37b748d81a9055e36a91f9f73267ac273b39a4cbea3736cdd1baa4c557e3059d8f3a9a9807d450e29f3d75c7cb18c94c37ded70768444313336
|
data/README.md
CHANGED
@@ -3,41 +3,48 @@
|
|
3
3
|
|
4
4
|
|
5
5
|
[![Gem Version](https://img.shields.io/gem/v/zeitwerk.svg?style=for-the-badge)](https://rubygems.org/gems/zeitwerk)
|
6
|
-
[![Build Status](https://img.shields.io/
|
6
|
+
[![Build Status](https://img.shields.io/github/workflow/status/fxn/zeitwerk/CI?event=push&style=for-the-badge)](https://github.com/fxn/zeitwerk/actions?query=event%3Apush)
|
7
7
|
|
8
8
|
<!-- TOC -->
|
9
9
|
|
10
10
|
- [Introduction](#introduction)
|
11
11
|
- [Synopsis](#synopsis)
|
12
12
|
- [File structure](#file-structure)
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
- [The idea: File paths match constant paths](#the-idea-file-paths-match-constant-paths)
|
14
|
+
- [Inner simple constants](#inner-simple-constants)
|
15
|
+
- [Root directories and root namespaces](#root-directories-and-root-namespaces)
|
16
|
+
- [The default root namespace is `Object`](#the-default-root-namespace-is-object)
|
17
|
+
- [Custom root namespaces](#custom-root-namespaces)
|
16
18
|
- [Nested root directories](#nested-root-directories)
|
19
|
+
- [Implicit namespaces](#implicit-namespaces)
|
20
|
+
- [Explicit namespaces](#explicit-namespaces)
|
21
|
+
- [Collapsing directories](#collapsing-directories)
|
17
22
|
- [Usage](#usage)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
- [
|
36
|
-
- [
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
23
|
+
- [Setup](#setup)
|
24
|
+
- [Generic](#generic)
|
25
|
+
- [for_gem](#for_gem)
|
26
|
+
- [Autoloading](#autoloading)
|
27
|
+
- [Eager loading](#eager-loading)
|
28
|
+
- [Reloading](#reloading)
|
29
|
+
- [Inflection](#inflection)
|
30
|
+
- [Zeitwerk::Inflector](#zeitwerkinflector)
|
31
|
+
- [Zeitwerk::GemInflector](#zeitwerkgeminflector)
|
32
|
+
- [Custom inflector](#custom-inflector)
|
33
|
+
- [The on_load callback](#the-on_load-callback)
|
34
|
+
- [The on_unload callback](#the-on_unload-callback)
|
35
|
+
- [Technical details](#technical-details)
|
36
|
+
- [Logging](#logging)
|
37
|
+
- [Loader tag](#loader-tag)
|
38
|
+
- [Ignoring parts of the project](#ignoring-parts-of-the-project)
|
39
|
+
- [Use case: Files that do not follow the conventions](#use-case-files-that-do-not-follow-the-conventions)
|
40
|
+
- [Use case: The adapter pattern](#use-case-the-adapter-pattern)
|
41
|
+
- [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
|
42
|
+
- [Edge cases](#edge-cases)
|
43
|
+
- [Reopening third-party namespaces](#reopening-third-party-namespaces)
|
44
|
+
- [Rules of thumb](#rules-of-thumb)
|
45
|
+
- [Debuggers](#debuggers)
|
46
|
+
- [Break](#break)
|
47
|
+
- [Byebug](#byebug)
|
41
48
|
- [Pronunciation](#pronunciation)
|
42
49
|
- [Supported Ruby versions](#supported-ruby-versions)
|
43
50
|
- [Testing](#testing)
|
@@ -117,6 +124,9 @@ Zeitwerk::Loader.eager_load_all
|
|
117
124
|
<a id="markdown-file-structure" name="file-structure"></a>
|
118
125
|
## File structure
|
119
126
|
|
127
|
+
<a id="markdown-the-idea-file-paths-match-constant-paths" name="the-idea-file-paths-match-constant-paths"></a>
|
128
|
+
### The idea: File paths match constant paths
|
129
|
+
|
120
130
|
To have a file structure Zeitwerk can work with, just name files and directories after the name of the classes and modules they define:
|
121
131
|
|
122
132
|
```
|
@@ -126,25 +136,57 @@ lib/my_gem/bar_baz.rb -> MyGem::BarBaz
|
|
126
136
|
lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
|
127
137
|
```
|
128
138
|
|
129
|
-
|
139
|
+
You can tune that a bit by [collapsing directories](#collapsing-directories), or by [ignoring parts of the project](#ignoring-parts-of-the-project), but that is the main idea.
|
140
|
+
|
141
|
+
<a id="markdown-inner-simple-constants" name="inner-simple-constants"></a>
|
142
|
+
### Inner simple constants
|
143
|
+
|
144
|
+
While a simple constant like `HttpCrawler::MAX_RETRIES` can be defined in its own file:
|
130
145
|
|
131
146
|
```ruby
|
132
|
-
|
133
|
-
|
147
|
+
# http_crawler/max_retries.rb
|
148
|
+
HttpCrawler::MAX_RETRIES = 10
|
134
149
|
```
|
135
150
|
|
136
|
-
|
151
|
+
that is not required, you can also define it the regular way:
|
137
152
|
|
153
|
+
```ruby
|
154
|
+
# http_crawler.rb
|
155
|
+
class HttpCrawler
|
156
|
+
MAX_RETRIES = 10
|
157
|
+
end
|
138
158
|
```
|
139
|
-
|
140
|
-
|
159
|
+
|
160
|
+
<a id="markdown-root-directories-and-root-namespaces" name="root-directories-and-root-namespaces"></a>
|
161
|
+
### Root directories and root namespaces
|
162
|
+
|
163
|
+
Every directory configured with `push_dir` is called a _root directory_, and they represent _root namespaces_.
|
164
|
+
|
165
|
+
<a id="markdown-the-default-root-namespace-is-object" name="the-default-root-namespace-is-object"></a>
|
166
|
+
#### The default root namespace is `Object`
|
167
|
+
|
168
|
+
By default, the namespace associated to a root directory is the top-level one: `Object`.
|
169
|
+
|
170
|
+
For example, given
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
loader.push_dir("#{__dir__}/models")
|
174
|
+
loader.push_dir("#{__dir__}/serializers"))
|
175
|
+
```
|
176
|
+
|
177
|
+
these are the expected classes and modules being defined by these files:
|
178
|
+
|
179
|
+
```
|
180
|
+
models/user.rb -> User
|
181
|
+
serializers/user_serializer.rb -> UserSerializer
|
141
182
|
```
|
142
183
|
|
143
|
-
|
184
|
+
<a id="markdown-custom-root-namespaces" name="custom-root-namespaces"></a>
|
185
|
+
#### Custom root namespaces
|
144
186
|
|
145
|
-
|
187
|
+
While `Object` is by far the most common root namespace, you can associate a different one to a particular root directory. The method `push_dir` accepts a class or module object in the optional `namespace` keyword argument.
|
146
188
|
|
147
|
-
|
189
|
+
For example, given:
|
148
190
|
|
149
191
|
```ruby
|
150
192
|
require "active_job"
|
@@ -152,9 +194,26 @@ require "active_job/queue_adapters"
|
|
152
194
|
loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
|
153
195
|
```
|
154
196
|
|
155
|
-
|
197
|
+
a file defining `ActiveJob::QueueAdapters::MyQueueAdapter` does not need the conventional parent directories, you can (and have to) store the file directly below `adapters`:
|
156
198
|
|
157
|
-
|
199
|
+
```
|
200
|
+
adapters/my_queue_adapter.rb -> ActiveJob::QueueAdapters::MyQueueAdapter
|
201
|
+
```
|
202
|
+
|
203
|
+
Please, note that the given root 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::Deliveries`, that one can be reloaded.
|
204
|
+
|
205
|
+
<a id="markdown-nested-root-directories" name="nested-root-directories"></a>
|
206
|
+
#### Nested root directories
|
207
|
+
|
208
|
+
Root directories should not be ideally nested, but Zeitwerk supports them because in Rails, for example, both `app/models` and `app/models/concerns` belong to the autoload paths.
|
209
|
+
|
210
|
+
Zeitwerk detects nested root directories, and treats them as roots only. In the example above, `concerns` is not considered to be a namespace below `app/models`. For example, the file:
|
211
|
+
|
212
|
+
```
|
213
|
+
app/models/concerns/geolocatable.rb
|
214
|
+
```
|
215
|
+
|
216
|
+
should define `Geolocatable`, not `Concerns::Geolocatable`.
|
158
217
|
|
159
218
|
<a id="markdown-implicit-namespaces" name="implicit-namespaces"></a>
|
160
219
|
### Implicit namespaces
|
@@ -216,19 +275,6 @@ To illustrate usage of glob patterns, if `actions` in the example above is part
|
|
216
275
|
loader.collapse("#{__dir__}/*/actions")
|
217
276
|
```
|
218
277
|
|
219
|
-
<a id="markdown-nested-root-directories" name="nested-root-directories"></a>
|
220
|
-
### Nested root directories
|
221
|
-
|
222
|
-
Root directories should not be ideally nested, but Zeitwerk supports them because in Rails, for example, both `app/models` and `app/models/concerns` belong to the autoload paths.
|
223
|
-
|
224
|
-
Zeitwerk detects nested root directories, and treats them as roots only. In the example above, `concerns` is not considered to be a namespace below `app/models`. For example, the file:
|
225
|
-
|
226
|
-
```
|
227
|
-
app/models/concerns/geolocatable.rb
|
228
|
-
```
|
229
|
-
|
230
|
-
should define `Geolocatable`, not `Concerns::Geolocatable`.
|
231
|
-
|
232
278
|
<a id="markdown-usage" name="usage"></a>
|
233
279
|
## Usage
|
234
280
|
|
@@ -522,26 +568,23 @@ With `on_load`, it is easy to schedule code at boot time that initializes `endpo
|
|
522
568
|
|
523
569
|
```ruby
|
524
570
|
# config/environments/development.rb
|
525
|
-
loader.on_load("SomeApiClient") do
|
526
|
-
|
571
|
+
loader.on_load("SomeApiClient") do |klass, _abspath|
|
572
|
+
klass.endpoint = "https://api.dev"
|
527
573
|
end
|
528
574
|
|
529
575
|
# config/environments/production.rb
|
530
|
-
loader.on_load("SomeApiClient") do
|
531
|
-
|
576
|
+
loader.on_load("SomeApiClient") do |klass, _abspath|
|
577
|
+
klass.endpoint = "https://api.prod"
|
532
578
|
end
|
533
579
|
```
|
534
580
|
|
535
|
-
|
581
|
+
Some uses cases:
|
536
582
|
|
537
583
|
* Doing something with an autoloadable class or module in a Rails application during initialization, in a way that plays well with reloading. As in the previous example.
|
538
584
|
* Delaying the execution of the block until the class is loaded for performance.
|
539
585
|
* Delaying the execution of the block until the class is loaded because it follows the adapter pattern and better not to load the class if the user does not need it.
|
540
|
-
* Etc.
|
541
586
|
|
542
|
-
|
543
|
-
|
544
|
-
`on_load` receives the name of the target class or module as a string. The given block is executed every time its corresponding file is loaded. That includes reloads.
|
587
|
+
`on_load` gets a target constant path as a string (e.g., "User", or "Service::NotificationsGateway"). When fired, its block receives the stored value, and the absolute path to the corresponding file or directory as a string. The callback is executed every time the target is loaded. That includes reloads.
|
545
588
|
|
546
589
|
Multiple callbacks on the same target are supported, and they run in order of definition.
|
547
590
|
|
@@ -549,6 +592,66 @@ The block is executed once the loader has loaded the target. In particular, if t
|
|
549
592
|
|
550
593
|
Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
|
551
594
|
|
595
|
+
It is also possible to be called when any constant managed by the loader is loaded:
|
596
|
+
|
597
|
+
```ruby
|
598
|
+
loader.on_load do |cpath, value, abspath|
|
599
|
+
# ...
|
600
|
+
end
|
601
|
+
```
|
602
|
+
|
603
|
+
The block gets the constant path as a string (e.g., "User", or "Foo::VERSION"), the value it stores (e.g., the class object stored in `User`, or "2.5.0"), and the absolute path to the corresponding file or directory as a string.
|
604
|
+
|
605
|
+
Multiple callbacks like these are supported, and they run in order of definition.
|
606
|
+
|
607
|
+
There are use cases for this last catch-all callback, but they are rare. If you just need to understand how things are being loaded for debugging purposes, please remember that `Zeitwerk::Loader#log!` logs plenty of information.
|
608
|
+
|
609
|
+
If both types of callbacks are defined, the specific ones run first.
|
610
|
+
|
611
|
+
<a id="markdown-the-on_unload-callback" name="the-on_unload-callback"></a>
|
612
|
+
### The on_unload callback
|
613
|
+
|
614
|
+
When reloading is enabled, you may occasionally need to execute something before a certain autoloaded class or module is unloaded. The `on_unload` callback allows you to do that.
|
615
|
+
|
616
|
+
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:
|
617
|
+
|
618
|
+
```ruby
|
619
|
+
loader.on_unload("Country") do |klass, _abspath|
|
620
|
+
klass.clear_cache
|
621
|
+
end
|
622
|
+
```
|
623
|
+
|
624
|
+
`on_unload` gets a target constant path as a string (e.g., "User", or "Service::NotificationsGateway"). When fired, its block receives the stored value, and the absolute path to the corresponding file or directory as a string. The callback is executed every time the target is unloaded.
|
625
|
+
|
626
|
+
`on_unload` blocks are executed before the class is unloaded, but in the middle of unloading, which happens in an unspecified order. Therefore, **that callback should not refer to any reloadable constant because there is no guarantee the constant works there**. Those blocks should rely on objects only, as in the example above, or regular constants not managed by the loader. This remark is transitive, applies to any methods invoked within the block.
|
627
|
+
|
628
|
+
Multiple callbacks on the same target are supported, and they run in order of definition.
|
629
|
+
|
630
|
+
Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
|
631
|
+
|
632
|
+
It is also possible to be called when any constant managed by the loader is unloaded:
|
633
|
+
|
634
|
+
```ruby
|
635
|
+
loader.on_unload do |cpath, value, abspath|
|
636
|
+
# ...
|
637
|
+
end
|
638
|
+
```
|
639
|
+
|
640
|
+
The block gets the constant path as a string (e.g., "User", or "Foo::VERSION"), the value it stores (e.g., the class object stored in `User`, or "2.5.0"), and the absolute path to the corresponding file or directory as a string.
|
641
|
+
|
642
|
+
Multiple callbacks like these are supported, and they run in order of definition.
|
643
|
+
|
644
|
+
If both types of callbacks are defined, the specific ones run first.
|
645
|
+
|
646
|
+
<a id="markdown-technical-details" name="technical-details"></a>
|
647
|
+
#### Technical details
|
648
|
+
|
649
|
+
Zeitwerk uses the word "unload" to ease communication and for symmetry with `on_load`. However, in Ruby you cannot unload things for real. So, when does `on_unload` technically happen?
|
650
|
+
|
651
|
+
When unloading, Zeitwerk issues `Module#remove_const` calls. Classes and modules are no longer reachable through their constants, and `on_unload` callbacks are executed right before those calls.
|
652
|
+
|
653
|
+
Technically, though, the objects themselves are still alive, but if everything is used as expected and they are not stored in any non-reloadable place (don't do that), they are ready for garbage collection, which is when the real unloading happens.
|
654
|
+
|
552
655
|
<a id="markdown-logging" name="logging"></a>
|
553
656
|
### Logging
|
554
657
|
|
data/lib/zeitwerk.rb
CHANGED
@@ -3,10 +3,12 @@
|
|
3
3
|
module Zeitwerk
|
4
4
|
require_relative "zeitwerk/real_mod_name"
|
5
5
|
require_relative "zeitwerk/loader"
|
6
|
+
require_relative "zeitwerk/autoloads"
|
6
7
|
require_relative "zeitwerk/registry"
|
7
8
|
require_relative "zeitwerk/explicit_namespace"
|
8
9
|
require_relative "zeitwerk/inflector"
|
9
10
|
require_relative "zeitwerk/gem_inflector"
|
10
11
|
require_relative "zeitwerk/kernel"
|
11
12
|
require_relative "zeitwerk/error"
|
13
|
+
require_relative "zeitwerk/version"
|
12
14
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Zeitwerk
|
2
|
+
# @private
|
3
|
+
class Autoloads
|
4
|
+
# Maps crefs for which an autoload has been defined to the corresponding
|
5
|
+
# absolute path.
|
6
|
+
#
|
7
|
+
# [Object, :User] => "/Users/fxn/blog/app/models/user.rb"
|
8
|
+
# [Object, :Hotel] => "/Users/fxn/blog/app/models/hotel"
|
9
|
+
# ...
|
10
|
+
#
|
11
|
+
# This colection is transient, callbacks delete its entries as autoloads get
|
12
|
+
# executed.
|
13
|
+
#
|
14
|
+
# @sig Hash[[Module, Symbol], String]
|
15
|
+
attr_reader :c2a
|
16
|
+
|
17
|
+
# This is the inverse of c2a, for inverse lookups.
|
18
|
+
#
|
19
|
+
# @sig Hash[String, [Module, Symbol]]
|
20
|
+
attr_reader :a2c
|
21
|
+
|
22
|
+
# @sig () -> void
|
23
|
+
def initialize
|
24
|
+
@c2a = {}
|
25
|
+
@a2c = {}
|
26
|
+
end
|
27
|
+
|
28
|
+
# @sig (Module, Symbol, String) -> void
|
29
|
+
def define(parent, cname, abspath)
|
30
|
+
parent.autoload(cname, abspath)
|
31
|
+
cref = [parent, cname]
|
32
|
+
c2a[cref] = abspath
|
33
|
+
a2c[abspath] = cref
|
34
|
+
end
|
35
|
+
|
36
|
+
# @sig () { () -> [[Module, Symbol], String] } -> void
|
37
|
+
def each(&block)
|
38
|
+
c2a.each(&block)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @sig (Module, Symbol) -> String?
|
42
|
+
def abspath_for(parent, cname)
|
43
|
+
c2a[[parent, cname]]
|
44
|
+
end
|
45
|
+
|
46
|
+
# @sig (String) -> [Module, Symbol]?
|
47
|
+
def cref_for(abspath)
|
48
|
+
a2c[abspath]
|
49
|
+
end
|
50
|
+
|
51
|
+
# @sig (String) -> [Module, Symbol]?
|
52
|
+
def delete(abspath)
|
53
|
+
cref = a2c.delete(abspath)
|
54
|
+
c2a.delete(cref)
|
55
|
+
cref
|
56
|
+
end
|
57
|
+
|
58
|
+
# @sig () -> void
|
59
|
+
def clear
|
60
|
+
c2a.clear
|
61
|
+
a2c.clear
|
62
|
+
end
|
63
|
+
|
64
|
+
# @sig () -> bool
|
65
|
+
def empty?
|
66
|
+
c2a.empty? && a2c.empty?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -62,8 +62,14 @@ module Zeitwerk
|
|
62
62
|
# than accessing its name.
|
63
63
|
return if event.self.singleton_class?
|
64
64
|
|
65
|
-
#
|
66
|
-
#
|
65
|
+
# It might be tempting to return if name.nil?, to avoid the computation
|
66
|
+
# of a hash code and delete call. But Ruby does not trigger the :class
|
67
|
+
# event on Class.new or Module.new, so that would incur in an extra call
|
68
|
+
# for nothing.
|
69
|
+
#
|
70
|
+
# On the other hand, if we were called, cpaths is not empty. Otherwise
|
71
|
+
# the tracer is disabled. So we do need to go ahead with the hash code
|
72
|
+
# computation and delete call.
|
67
73
|
if loader = cpaths.delete(real_mod_name(event.self))
|
68
74
|
loader.on_namespace_loaded(event.self)
|
69
75
|
disable_tracer_if_unneeded
|
data/lib/zeitwerk/kernel.rb
CHANGED
@@ -12,7 +12,8 @@ module Kernel
|
|
12
12
|
# On the other hand, if you publish a new version of a gem that is now managed
|
13
13
|
# by Zeitwerk, client code can reference directly your classes and modules and
|
14
14
|
# should not require anything. But if someone has legacy require calls around,
|
15
|
-
# they will work as expected, and in a compatible way.
|
15
|
+
# they will work as expected, and in a compatible way. This feature is by now
|
16
|
+
# EXPERIMENTAL and UNDOCUMENTED.
|
16
17
|
#
|
17
18
|
# We cannot decorate with prepend + super because Kernel has already been
|
18
19
|
# included in Object, and changes in ancestors don't get propagated into
|
@@ -33,9 +34,9 @@ module Kernel
|
|
33
34
|
else
|
34
35
|
zeitwerk_original_require(path).tap do |required|
|
35
36
|
if required
|
36
|
-
|
37
|
-
if loader = Zeitwerk::Registry.loader_for(
|
38
|
-
loader.on_file_autoloaded(
|
37
|
+
abspath = $LOADED_FEATURES.last
|
38
|
+
if loader = Zeitwerk::Registry.loader_for(abspath)
|
39
|
+
loader.on_file_autoloaded(abspath)
|
39
40
|
end
|
40
41
|
end
|
41
42
|
end
|
data/lib/zeitwerk/loader.rb
CHANGED
@@ -5,78 +5,31 @@ require "securerandom"
|
|
5
5
|
|
6
6
|
module Zeitwerk
|
7
7
|
class Loader
|
8
|
+
require_relative "loader/helpers"
|
8
9
|
require_relative "loader/callbacks"
|
9
|
-
|
10
|
-
include RealModName
|
11
|
-
|
12
|
-
# @sig String
|
13
|
-
attr_reader :tag
|
10
|
+
require_relative "loader/config"
|
14
11
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
attr_accessor :logger
|
12
|
+
include RealModName
|
13
|
+
include Callbacks
|
14
|
+
include Helpers
|
15
|
+
include Config
|
20
16
|
|
21
|
-
#
|
22
|
-
#
|
23
|
-
# needed for detecting nested paths.
|
17
|
+
# Keeps track of autoloads defined by the loader which have not been
|
18
|
+
# executed so far.
|
24
19
|
#
|
25
|
-
#
|
26
|
-
# "/Users/fxn/blog/app/channels" => true,
|
27
|
-
# ...
|
20
|
+
# This metadata helps us implement a few things:
|
28
21
|
#
|
29
|
-
#
|
30
|
-
#
|
22
|
+
# 1. When autoloads are triggered, ensure they define the expected constant
|
23
|
+
# and invoke user callbacks. If reloading is enabled, remember cref and
|
24
|
+
# abspath for later unloading logic.
|
31
25
|
#
|
32
|
-
#
|
33
|
-
# @sig Hash[String, true]
|
34
|
-
attr_reader :root_dirs
|
35
|
-
|
36
|
-
# Absolute paths of files or directories that have to be preloaded.
|
26
|
+
# 2. When unloading, remove autoloads that have not been executed.
|
37
27
|
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
attr_reader :preloads
|
41
|
-
|
42
|
-
# Absolute paths of files, directories, or glob patterns to be totally
|
43
|
-
# ignored.
|
28
|
+
# 3. Eager load with a recursive const_get, rather than a recursive require,
|
29
|
+
# for consistency with lazy loading.
|
44
30
|
#
|
45
31
|
# @private
|
46
|
-
# @sig
|
47
|
-
attr_reader :ignored_glob_patterns
|
48
|
-
|
49
|
-
# The actual collection of absolute file and directory names at the time the
|
50
|
-
# ignored glob patterns were expanded. Computed on setup, and recomputed on
|
51
|
-
# reload.
|
52
|
-
#
|
53
|
-
# @private
|
54
|
-
# @sig Set[String]
|
55
|
-
attr_reader :ignored_paths
|
56
|
-
|
57
|
-
# Absolute paths of directories or glob patterns to be collapsed.
|
58
|
-
#
|
59
|
-
# @private
|
60
|
-
# @sig 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
|
-
# @sig Set[String]
|
68
|
-
attr_reader :collapse_dirs
|
69
|
-
|
70
|
-
# Maps real absolute paths for which an autoload has been set ---and not
|
71
|
-
# executed--- to their corresponding parent class or module and constant
|
72
|
-
# name.
|
73
|
-
#
|
74
|
-
# "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
|
75
|
-
# "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
|
76
|
-
# ...
|
77
|
-
#
|
78
|
-
# @private
|
79
|
-
# @sig Hash[String, [Module, Symbol]]
|
32
|
+
# @sig Zeitwerk::Autoloads
|
80
33
|
attr_reader :autoloads
|
81
34
|
|
82
35
|
# We keep track of autoloaded directories to remove them from the registry
|
@@ -93,8 +46,8 @@ module Zeitwerk
|
|
93
46
|
#
|
94
47
|
# "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
|
95
48
|
#
|
96
|
-
# The cpath as key helps implementing unloadable_cpath? The
|
97
|
-
#
|
49
|
+
# The cpath as key helps implementing unloadable_cpath? The file name is
|
50
|
+
# stored in order to be able to delete it from $LOADED_FEATURES, and the
|
98
51
|
# pair [Module, Symbol] is used to remove_const the constant from the class
|
99
52
|
# or module object.
|
100
53
|
#
|
@@ -123,15 +76,6 @@ module Zeitwerk
|
|
123
76
|
# @sig Hash[String, Array[String]]
|
124
77
|
attr_reader :lazy_subdirs
|
125
78
|
|
126
|
-
# Absolute paths of files or directories not to be eager loaded.
|
127
|
-
#
|
128
|
-
# @private
|
129
|
-
# @sig Set[String]
|
130
|
-
attr_reader :eager_load_exclusions
|
131
|
-
|
132
|
-
# User-oriented callbacks to be fired when a constant is loaded.
|
133
|
-
attr_reader :on_load_callbacks
|
134
|
-
|
135
79
|
# @private
|
136
80
|
# @sig Mutex
|
137
81
|
attr_reader :mutex
|
@@ -141,150 +85,21 @@ module Zeitwerk
|
|
141
85
|
attr_reader :mutex2
|
142
86
|
|
143
87
|
def initialize
|
144
|
-
|
145
|
-
|
146
|
-
@tag = SecureRandom.hex(3)
|
147
|
-
@inflector = Inflector.new
|
148
|
-
@logger = self.class.default_logger
|
149
|
-
|
150
|
-
@root_dirs = {}
|
151
|
-
@preloads = []
|
152
|
-
@ignored_glob_patterns = Set.new
|
153
|
-
@ignored_paths = Set.new
|
154
|
-
@collapse_glob_patterns = Set.new
|
155
|
-
@collapse_dirs = Set.new
|
156
|
-
@autoloads = {}
|
157
|
-
@autoloaded_dirs = []
|
158
|
-
@to_unload = {}
|
159
|
-
@lazy_subdirs = {}
|
160
|
-
@eager_load_exclusions = Set.new
|
161
|
-
@on_load_callbacks = {}
|
162
|
-
|
163
|
-
# TODO: find a better name for these mutexes.
|
164
|
-
@mutex = Mutex.new
|
165
|
-
@mutex2 = Mutex.new
|
166
|
-
@setup = false
|
167
|
-
@eager_loaded = false
|
168
|
-
|
169
|
-
@reloading_enabled = false
|
170
|
-
|
171
|
-
Registry.register_loader(self)
|
172
|
-
end
|
173
|
-
|
174
|
-
# Sets a tag for the loader, useful for logging.
|
175
|
-
#
|
176
|
-
# @param tag [#to_s]
|
177
|
-
# @sig (#to_s) -> void
|
178
|
-
def tag=(tag)
|
179
|
-
@tag = tag.to_s
|
180
|
-
end
|
181
|
-
|
182
|
-
# Absolute paths of the root directories. This is a read-only collection,
|
183
|
-
# please push here via `push_dir`.
|
184
|
-
#
|
185
|
-
# @sig () -> Array[String]
|
186
|
-
def dirs
|
187
|
-
root_dirs.keys.freeze
|
188
|
-
end
|
189
|
-
|
190
|
-
# Pushes `path` to the list of root directories.
|
191
|
-
#
|
192
|
-
# Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in
|
193
|
-
# the same process already manages that directory or one of its ascendants
|
194
|
-
# or descendants.
|
195
|
-
#
|
196
|
-
# @raise [Zeitwerk::Error]
|
197
|
-
# @sig (String | Pathname, Module) -> void
|
198
|
-
def push_dir(path, namespace: Object)
|
199
|
-
# Note that Class < Module.
|
200
|
-
unless namespace.is_a?(Module)
|
201
|
-
raise Error, "#{namespace.inspect} is not a class or module object, should be"
|
202
|
-
end
|
88
|
+
super
|
203
89
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
90
|
+
@autoloads = Autoloads.new
|
91
|
+
@autoloaded_dirs = []
|
92
|
+
@to_unload = {}
|
93
|
+
@lazy_subdirs = Hash.new { |h, cpath| h[cpath] = [] }
|
94
|
+
@mutex = Mutex.new
|
95
|
+
@mutex2 = Mutex.new
|
96
|
+
@setup = false
|
97
|
+
@eager_loaded = false
|
212
98
|
|
213
|
-
|
214
|
-
# There is no way to undo this, either you want to reload or you don't.
|
215
|
-
#
|
216
|
-
# @raise [Zeitwerk::Error]
|
217
|
-
# @sig () -> void
|
218
|
-
def enable_reloading
|
219
|
-
mutex.synchronize do
|
220
|
-
break if @reloading_enabled
|
221
|
-
|
222
|
-
if @setup
|
223
|
-
raise Error, "cannot enable reloading after setup"
|
224
|
-
else
|
225
|
-
@reloading_enabled = true
|
226
|
-
end
|
227
|
-
end
|
228
|
-
end
|
229
|
-
|
230
|
-
# @sig () -> bool
|
231
|
-
def reloading_enabled?
|
232
|
-
@reloading_enabled
|
233
|
-
end
|
234
|
-
|
235
|
-
# Files or directories to be preloaded instead of lazy loaded.
|
236
|
-
#
|
237
|
-
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
238
|
-
def preload(*paths)
|
239
|
-
mutex.synchronize do
|
240
|
-
expand_paths(paths).each do |abspath|
|
241
|
-
preloads << abspath
|
242
|
-
do_preload_abspath(abspath) if @setup
|
243
|
-
end
|
244
|
-
end
|
245
|
-
end
|
246
|
-
|
247
|
-
# Configure files, directories, or glob patterns to be totally ignored.
|
248
|
-
#
|
249
|
-
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
250
|
-
def ignore(*glob_patterns)
|
251
|
-
glob_patterns = expand_paths(glob_patterns)
|
252
|
-
mutex.synchronize do
|
253
|
-
ignored_glob_patterns.merge(glob_patterns)
|
254
|
-
ignored_paths.merge(expand_glob_patterns(glob_patterns))
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
# Configure directories or glob patterns to be collapsed.
|
259
|
-
#
|
260
|
-
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
261
|
-
def collapse(*glob_patterns)
|
262
|
-
glob_patterns = expand_paths(glob_patterns)
|
263
|
-
mutex.synchronize do
|
264
|
-
collapse_glob_patterns.merge(glob_patterns)
|
265
|
-
collapse_dirs.merge(expand_glob_patterns(glob_patterns))
|
266
|
-
end
|
267
|
-
end
|
268
|
-
|
269
|
-
# Configure a block to be invoked once a certain constant path is loaded.
|
270
|
-
# Supports multiple callbacks, and if there are many, they are executed in
|
271
|
-
# the order in which they were defined.
|
272
|
-
#
|
273
|
-
# loader.on_load("SomeApiClient") do
|
274
|
-
# SomeApiClient.endpoint = "https://api.dev"
|
275
|
-
# end
|
276
|
-
#
|
277
|
-
# @raise [TypeError]
|
278
|
-
# @sig (String) { () -> void } -> void
|
279
|
-
def on_load(cpath, &block)
|
280
|
-
raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String)
|
281
|
-
|
282
|
-
mutex.synchronize do
|
283
|
-
(on_load_callbacks[cpath] ||= []) << block
|
284
|
-
end
|
99
|
+
Registry.register_loader(self)
|
285
100
|
end
|
286
101
|
|
287
|
-
# Sets autoloads in the root namespace
|
102
|
+
# Sets autoloads in the root namespace.
|
288
103
|
#
|
289
104
|
# @sig () -> void
|
290
105
|
def setup
|
@@ -294,7 +109,6 @@ module Zeitwerk
|
|
294
109
|
actual_root_dirs.each do |root_dir, namespace|
|
295
110
|
set_autoloads_in_dir(root_dir, namespace)
|
296
111
|
end
|
297
|
-
do_preload
|
298
112
|
|
299
113
|
@setup = true
|
300
114
|
end
|
@@ -319,21 +133,26 @@ module Zeitwerk
|
|
319
133
|
# is enough.
|
320
134
|
unloaded_files = Set.new
|
321
135
|
|
322
|
-
autoloads.each do |
|
136
|
+
autoloads.each do |(parent, cname), abspath|
|
323
137
|
if parent.autoload?(cname)
|
324
138
|
unload_autoload(parent, cname)
|
325
139
|
else
|
326
140
|
# Could happen if loaded with require_relative. That is unsupported,
|
327
141
|
# and the constant path would escape unloadable_cpath? This is just
|
328
142
|
# defensive code to clean things up as much as we are able to.
|
329
|
-
unload_cref(parent, cname)
|
330
|
-
unloaded_files.add(
|
143
|
+
unload_cref(parent, cname) if cdef?(parent, cname)
|
144
|
+
unloaded_files.add(abspath) if ruby?(abspath)
|
331
145
|
end
|
332
146
|
end
|
333
147
|
|
334
|
-
to_unload.
|
335
|
-
|
336
|
-
|
148
|
+
to_unload.each do |cpath, (abspath, (parent, cname))|
|
149
|
+
unless on_unload_callbacks.empty?
|
150
|
+
value = parent.const_get(cname)
|
151
|
+
run_on_unload_callbacks(cpath, value, abspath)
|
152
|
+
end
|
153
|
+
|
154
|
+
unload_cref(parent, cname) if cdef?(parent, cname)
|
155
|
+
unloaded_files.add(abspath) if ruby?(abspath)
|
337
156
|
end
|
338
157
|
|
339
158
|
unless unloaded_files.empty?
|
@@ -393,27 +212,29 @@ module Zeitwerk
|
|
393
212
|
mutex.synchronize do
|
394
213
|
break if @eager_loaded
|
395
214
|
|
215
|
+
log("eager load start") if logger
|
216
|
+
|
396
217
|
queue = []
|
397
218
|
actual_root_dirs.each do |root_dir, namespace|
|
398
|
-
queue << [namespace, root_dir] unless
|
219
|
+
queue << [namespace, root_dir] unless excluded_from_eager_load?(root_dir)
|
399
220
|
end
|
400
221
|
|
401
222
|
while to_eager_load = queue.shift
|
402
223
|
namespace, dir = to_eager_load
|
403
224
|
|
404
225
|
ls(dir) do |basename, abspath|
|
405
|
-
next if
|
226
|
+
next if excluded_from_eager_load?(abspath)
|
406
227
|
|
407
228
|
if ruby?(abspath)
|
408
|
-
if cref = autoloads
|
409
|
-
|
229
|
+
if cref = autoloads.cref_for(abspath)
|
230
|
+
cget(*cref)
|
410
231
|
end
|
411
232
|
elsif dir?(abspath) && !root_dirs.key?(abspath)
|
412
|
-
if
|
233
|
+
if collapse?(abspath)
|
413
234
|
queue << [namespace, abspath]
|
414
235
|
else
|
415
236
|
cname = inflector.camelize(basename, abspath)
|
416
|
-
queue << [namespace
|
237
|
+
queue << [cget(namespace, cname), abspath]
|
417
238
|
end
|
418
239
|
end
|
419
240
|
end
|
@@ -425,15 +246,9 @@ module Zeitwerk
|
|
425
246
|
autoloaded_dirs.clear
|
426
247
|
|
427
248
|
@eager_loaded = true
|
428
|
-
end
|
429
|
-
end
|
430
249
|
|
431
|
-
|
432
|
-
|
433
|
-
#
|
434
|
-
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
435
|
-
def do_not_eager_load(*paths)
|
436
|
-
mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
|
250
|
+
log("eager load end") if logger
|
251
|
+
end
|
437
252
|
end
|
438
253
|
|
439
254
|
# Says if the given constant path would be unloaded on reload. This
|
@@ -452,28 +267,6 @@ module Zeitwerk
|
|
452
267
|
to_unload.keys.freeze
|
453
268
|
end
|
454
269
|
|
455
|
-
# Logs to `$stdout`, handy shortcut for debugging.
|
456
|
-
#
|
457
|
-
# @sig () -> void
|
458
|
-
def log!
|
459
|
-
@logger = ->(msg) { puts msg }
|
460
|
-
end
|
461
|
-
|
462
|
-
# @private
|
463
|
-
# @sig (String) -> bool
|
464
|
-
def manages?(dir)
|
465
|
-
dir = dir + "/"
|
466
|
-
ignored_paths.each do |ignored_path|
|
467
|
-
return false if dir.start_with?(ignored_path + "/")
|
468
|
-
end
|
469
|
-
|
470
|
-
root_dirs.each_key do |root_dir|
|
471
|
-
return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
|
472
|
-
end
|
473
|
-
|
474
|
-
false
|
475
|
-
end
|
476
|
-
|
477
270
|
# --- Class methods ---------------------------------------------------------------------------
|
478
271
|
|
479
272
|
class << self
|
@@ -521,19 +314,12 @@ module Zeitwerk
|
|
521
314
|
|
522
315
|
private # -------------------------------------------------------------------------------------
|
523
316
|
|
524
|
-
# @sig () -> Array[String]
|
525
|
-
def actual_root_dirs
|
526
|
-
root_dirs.reject do |root_dir, _namespace|
|
527
|
-
!dir?(root_dir) || ignored_paths.member?(root_dir)
|
528
|
-
end
|
529
|
-
end
|
530
|
-
|
531
317
|
# @sig (String, Module) -> void
|
532
318
|
def set_autoloads_in_dir(dir, parent)
|
533
319
|
ls(dir) do |basename, abspath|
|
534
320
|
begin
|
535
321
|
if ruby?(basename)
|
536
|
-
basename
|
322
|
+
basename.delete_suffix!(".rb")
|
537
323
|
cname = inflector.camelize(basename, abspath).to_sym
|
538
324
|
autoload_file(parent, cname, abspath)
|
539
325
|
elsif dir?(abspath)
|
@@ -543,9 +329,9 @@ module Zeitwerk
|
|
543
329
|
# To resolve the ambiguity file name -> constant path this introduces,
|
544
330
|
# the `app/models/concerns` directory is totally ignored as a namespace,
|
545
331
|
# it counts only as root. The guard checks that.
|
546
|
-
unless
|
332
|
+
unless root_dir?(abspath)
|
547
333
|
cname = inflector.camelize(basename, abspath).to_sym
|
548
|
-
if
|
334
|
+
if collapse?(abspath)
|
549
335
|
set_autoloads_in_dir(abspath, parent)
|
550
336
|
else
|
551
337
|
autoload_subdir(parent, cname, abspath)
|
@@ -573,27 +359,28 @@ module Zeitwerk
|
|
573
359
|
|
574
360
|
# @sig (Module, Symbol, String) -> void
|
575
361
|
def autoload_subdir(parent, cname, subdir)
|
576
|
-
if autoload_path =
|
362
|
+
if autoload_path = autoloads.abspath_for(parent, cname)
|
577
363
|
cpath = cpath(parent, cname)
|
578
364
|
register_explicit_namespace(cpath) if ruby?(autoload_path)
|
579
365
|
# We do not need to issue another autoload, the existing one is enough
|
580
366
|
# no matter if it is for a file or a directory. Just remember the
|
581
367
|
# subdirectory has to be visited if the namespace is used.
|
582
|
-
|
368
|
+
lazy_subdirs[cpath] << subdir
|
583
369
|
elsif !cdef?(parent, cname)
|
584
370
|
# First time we find this namespace, set an autoload for it.
|
585
|
-
|
371
|
+
lazy_subdirs[cpath(parent, cname)] << subdir
|
586
372
|
set_autoload(parent, cname, subdir)
|
587
373
|
else
|
588
374
|
# For whatever reason the constant that corresponds to this namespace has
|
589
375
|
# already been defined, we have to recurse.
|
590
|
-
|
376
|
+
log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
|
377
|
+
set_autoloads_in_dir(subdir, cget(parent, cname))
|
591
378
|
end
|
592
379
|
end
|
593
380
|
|
594
381
|
# @sig (Module, Symbol, String) -> void
|
595
382
|
def autoload_file(parent, cname, file)
|
596
|
-
if autoload_path =
|
383
|
+
if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
597
384
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
598
385
|
if ruby?(autoload_path)
|
599
386
|
log("file #{file} is ignored because #{autoload_path} has precedence") if logger
|
@@ -620,163 +407,32 @@ module Zeitwerk
|
|
620
407
|
autoloads.delete(dir)
|
621
408
|
Registry.unregister_autoload(dir)
|
622
409
|
|
410
|
+
log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
|
411
|
+
|
623
412
|
set_autoload(parent, cname, file)
|
624
413
|
register_explicit_namespace(cpath(parent, cname))
|
625
414
|
end
|
626
415
|
|
627
416
|
# @sig (Module, Symbol, String) -> void
|
628
417
|
def set_autoload(parent, cname, abspath)
|
629
|
-
|
630
|
-
|
631
|
-
# be able to do a lookup later in Kernel#require for manual require calls.
|
632
|
-
#
|
633
|
-
# We freeze realpath because that saves allocations in Module#autoload.
|
634
|
-
# See #125.
|
635
|
-
realpath = File.realpath(abspath).freeze
|
636
|
-
parent.autoload(cname, realpath)
|
418
|
+
autoloads.define(parent, cname, abspath)
|
419
|
+
|
637
420
|
if logger
|
638
|
-
if ruby?(
|
639
|
-
log("autoload set for #{cpath(parent, cname)}, to be loaded from #{
|
421
|
+
if ruby?(abspath)
|
422
|
+
log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
|
640
423
|
else
|
641
|
-
log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{
|
424
|
+
log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
|
642
425
|
end
|
643
426
|
end
|
644
427
|
|
645
|
-
|
646
|
-
Registry.register_autoload(self, realpath)
|
428
|
+
Registry.register_autoload(self, abspath)
|
647
429
|
|
648
430
|
# See why in the documentation of Zeitwerk::Registry.inceptions.
|
649
431
|
unless parent.autoload?(cname)
|
650
|
-
Registry.register_inception(cpath(parent, cname),
|
651
|
-
end
|
652
|
-
end
|
653
|
-
|
654
|
-
# @sig (Module, Symbol) -> String?
|
655
|
-
def autoload_for?(parent, cname)
|
656
|
-
strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
657
|
-
end
|
658
|
-
|
659
|
-
# The autoload? predicate takes into account the ancestor chain of the
|
660
|
-
# receiver, like const_defined? and other methods in the constants API do.
|
661
|
-
#
|
662
|
-
# For example, given
|
663
|
-
#
|
664
|
-
# class A
|
665
|
-
# autoload :X, "x.rb"
|
666
|
-
# end
|
667
|
-
#
|
668
|
-
# class B < A
|
669
|
-
# end
|
670
|
-
#
|
671
|
-
# B.autoload?(:X) returns "x.rb".
|
672
|
-
#
|
673
|
-
# We need a way to strictly check in parent ignoring ancestors.
|
674
|
-
#
|
675
|
-
# @sig (Module, Symbol) -> String?
|
676
|
-
if method(:autoload?).arity == 1
|
677
|
-
def strict_autoload_path(parent, cname)
|
678
|
-
parent.autoload?(cname) if cdef?(parent, cname)
|
679
|
-
end
|
680
|
-
else
|
681
|
-
def strict_autoload_path(parent, cname)
|
682
|
-
parent.autoload?(cname, false)
|
683
|
-
end
|
684
|
-
end
|
685
|
-
|
686
|
-
# This method is called this way because I prefer `preload` to be the method
|
687
|
-
# name to configure preloads in the public interface.
|
688
|
-
#
|
689
|
-
# @sig () -> void
|
690
|
-
def do_preload
|
691
|
-
preloads.each do |abspath|
|
692
|
-
do_preload_abspath(abspath)
|
432
|
+
Registry.register_inception(cpath(parent, cname), abspath, self)
|
693
433
|
end
|
694
434
|
end
|
695
435
|
|
696
|
-
# @sig (String) -> void
|
697
|
-
def do_preload_abspath(abspath)
|
698
|
-
if ruby?(abspath)
|
699
|
-
do_preload_file(abspath)
|
700
|
-
elsif dir?(abspath)
|
701
|
-
do_preload_dir(abspath)
|
702
|
-
end
|
703
|
-
end
|
704
|
-
|
705
|
-
# @sig (String) -> void
|
706
|
-
def do_preload_dir(dir)
|
707
|
-
ls(dir) do |_basename, abspath|
|
708
|
-
do_preload_abspath(abspath)
|
709
|
-
end
|
710
|
-
end
|
711
|
-
|
712
|
-
# @sig (String) -> bool
|
713
|
-
def do_preload_file(file)
|
714
|
-
log("preloading #{file}") if logger
|
715
|
-
require file
|
716
|
-
end
|
717
|
-
|
718
|
-
# @sig (Module, Symbol) -> String
|
719
|
-
def cpath(parent, cname)
|
720
|
-
parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
|
721
|
-
end
|
722
|
-
|
723
|
-
# @sig (String) { (String, String) -> void } -> void
|
724
|
-
def ls(dir)
|
725
|
-
Dir.foreach(dir) do |basename|
|
726
|
-
next if basename.start_with?(".")
|
727
|
-
|
728
|
-
abspath = File.join(dir, basename)
|
729
|
-
next if ignored_paths.member?(abspath)
|
730
|
-
|
731
|
-
# We freeze abspath because that saves allocations when passed later to
|
732
|
-
# File methods. See #125.
|
733
|
-
yield basename, abspath.freeze
|
734
|
-
end
|
735
|
-
end
|
736
|
-
|
737
|
-
# @sig (String) -> bool
|
738
|
-
def ruby?(path)
|
739
|
-
path.end_with?(".rb")
|
740
|
-
end
|
741
|
-
|
742
|
-
# @sig (String) -> bool
|
743
|
-
def dir?(path)
|
744
|
-
File.directory?(path)
|
745
|
-
end
|
746
|
-
|
747
|
-
# @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
|
748
|
-
def expand_paths(paths)
|
749
|
-
paths.flatten.map! { |path| File.expand_path(path) }
|
750
|
-
end
|
751
|
-
|
752
|
-
# @sig (Array[String]) -> Array[String]
|
753
|
-
def expand_glob_patterns(glob_patterns)
|
754
|
-
# Note that Dir.glob works with regular file names just fine. That is,
|
755
|
-
# glob patterns technically need no wildcards.
|
756
|
-
glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
|
757
|
-
end
|
758
|
-
|
759
|
-
# @sig () -> void
|
760
|
-
def recompute_ignored_paths
|
761
|
-
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
762
|
-
end
|
763
|
-
|
764
|
-
# @sig () -> void
|
765
|
-
def recompute_collapse_dirs
|
766
|
-
collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
|
767
|
-
end
|
768
|
-
|
769
|
-
# @sig (String) -> void
|
770
|
-
def log(message)
|
771
|
-
method_name = logger.respond_to?(:debug) ? :debug : :call
|
772
|
-
logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
|
773
|
-
end
|
774
|
-
|
775
|
-
# @sig (Module, Symbol) -> bool
|
776
|
-
def cdef?(parent, cname)
|
777
|
-
parent.const_defined?(cname, false)
|
778
|
-
end
|
779
|
-
|
780
436
|
# @sig (String) -> void
|
781
437
|
def register_explicit_namespace(cpath)
|
782
438
|
ExplicitNamespace.register(cpath, self)
|
@@ -797,6 +453,13 @@ module Zeitwerk
|
|
797
453
|
end
|
798
454
|
end
|
799
455
|
|
456
|
+
# @sig (String, Object, String) -> void
|
457
|
+
def run_on_unload_callbacks(cpath, value, abspath)
|
458
|
+
# Order matters. If present, run the most specific one.
|
459
|
+
on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
|
460
|
+
on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
|
461
|
+
end
|
462
|
+
|
800
463
|
# @sig (Module, Symbol) -> void
|
801
464
|
def unload_autoload(parent, cname)
|
802
465
|
parent.__send__(:remove_const, cname)
|