zeitwerk 1.0.0.alpha → 1.0.0.beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +141 -128
- data/lib/zeitwerk/gem_inflector.rb +5 -4
- data/lib/zeitwerk/inflector.rb +2 -4
- data/lib/zeitwerk/loader.rb +44 -47
- data/lib/zeitwerk/registry.rb +16 -21
- data/lib/zeitwerk/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb5151594ce18ccd7919a1ed6b7860107ce1bfc1a02a4f9b038d77cc91ce2450
|
4
|
+
data.tar.gz: e403ef92cc0a14b09bcb54d15d53ba821ad3d72e7d6216c3b0e10e6e9c0734f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 10f8af59a66e1462c622cfb6f5316c98808113830041f094861b1fa07a2e9a6734d1ea64a01ebc664e9b5dcf20844b443e98c6bbf196fec99a0dcfee76010331
|
7
|
+
data.tar.gz: 226489835ceb91d696138a966b34054755d9fb1e6bb3245cc028d03e156f21df6a949ee329f5575be1501e8bc143afe532037ff95af2a3631e82fe72963e9479
|
data/README.md
CHANGED
@@ -1,22 +1,45 @@
|
|
1
|
-
# WIP — NOT PUBLISHED — API AND DOCS IN FLUX
|
2
|
-
|
3
1
|
# Zeitwerk
|
4
2
|
|
5
3
|
[](https://travis-ci.com/fxn/zeitwerk)
|
6
4
|
|
5
|
+
<!-- TOC -->
|
6
|
+
|
7
|
+
- [Zeitwerk](#zeitwerk)
|
8
|
+
- [Introduction](#introduction)
|
9
|
+
- [Synopsis](#synopsis)
|
10
|
+
- [File structure](#file-structure)
|
11
|
+
- [Implicit namespaces](#implicit-namespaces)
|
12
|
+
- [Explicit namespaces](#explicit-namespaces)
|
13
|
+
- [Nested root directories](#nested-root-directories)
|
14
|
+
- [Usage](#usage)
|
15
|
+
- [Setup](#setup)
|
16
|
+
- [Reloading](#reloading)
|
17
|
+
- [Eager loading](#eager-loading)
|
18
|
+
- [Preloading](#preloading)
|
19
|
+
- [Inflection](#inflection)
|
20
|
+
- [Zeitwerk::Inflector](#zeitwerkinflector)
|
21
|
+
- [Zeitwerk::GemInflector](#zeitwerkgeminflector)
|
22
|
+
- [Custom inflector](#custom-inflector)
|
23
|
+
- [Logging](#logging)
|
24
|
+
- [Ignoring parts of the project](#ignoring-parts-of-the-project)
|
25
|
+
- [Supported Ruby versions](#supported-ruby-versions)
|
26
|
+
- [Motivation](#motivation)
|
27
|
+
- [Thanks](#thanks)
|
28
|
+
- [License](#license)
|
29
|
+
|
30
|
+
<!-- /TOC -->
|
31
|
+
|
7
32
|
## Introduction
|
8
33
|
|
9
34
|
Zeitwerk is an efficient and thread-safe code loader for Ruby.
|
10
35
|
|
11
|
-
Given a conventional file structure, Zeitwerk loads
|
12
|
-
|
13
|
-
This feature is efficient, thread-safe, and matches Ruby's semantics for constants.
|
36
|
+
Given a conventional file structure, Zeitwerk loads your project's classes and modules on demand. You don't need to write `require` calls for your own files, rather, you can streamline your programming knowing that your classes and modules are available everywhere. This feature is efficient, thread-safe, and matches Ruby's semantics for constants.
|
14
37
|
|
15
38
|
The library is designed so that each gem and application can have their own loader, independent of each other. Each loader has its own configuration, inflector, and optional logger.
|
16
39
|
|
17
40
|
Zeitwerk is also able to reload code, which may be handy for web applications. Coordination is needed to reload in a thread-safe manner. The documentation below explains how to do this.
|
18
41
|
|
19
|
-
Finally, in some production setups it may be
|
42
|
+
Finally, in some production setups it may be optimal to eager load all code upfront. Zeitwerk is able to do that too.
|
20
43
|
|
21
44
|
## Synopsis
|
22
45
|
|
@@ -26,7 +49,7 @@ Main interface for gems:
|
|
26
49
|
# lib/my_gem.rb (main file)
|
27
50
|
|
28
51
|
require "zeitwerk"
|
29
|
-
Zeitwerk::Loader.for_gem.setup
|
52
|
+
Zeitwerk::Loader.for_gem.setup # ready!
|
30
53
|
|
31
54
|
module MyGem
|
32
55
|
# ...
|
@@ -38,10 +61,10 @@ Main generic interface:
|
|
38
61
|
```ruby
|
39
62
|
loader = Zeitwerk::Loader.new
|
40
63
|
loader.push_dir(...)
|
41
|
-
loader.setup
|
64
|
+
loader.setup # ready!
|
42
65
|
```
|
43
66
|
|
44
|
-
Zeitwerk
|
67
|
+
The `loader` variable can go out of scope. Zeitwerk keeps a registry with all of them, and so the object won't be garbage collected and remain active.
|
45
68
|
|
46
69
|
Later, you can reload if you want to:
|
47
70
|
|
@@ -49,21 +72,21 @@ Later, you can reload if you want to:
|
|
49
72
|
loader.reload
|
50
73
|
```
|
51
74
|
|
52
|
-
and you can also eager load:
|
75
|
+
and you can also eager load all the code:
|
53
76
|
|
54
77
|
```ruby
|
55
78
|
loader.eager_load
|
56
79
|
```
|
57
80
|
|
58
|
-
|
81
|
+
It is also possible to broadcast `eager_load` to all instances:
|
59
82
|
|
60
83
|
```
|
61
84
|
Zeitwerk::Loader.eager_load_all
|
62
85
|
```
|
63
86
|
|
64
|
-
##
|
87
|
+
## File structure
|
65
88
|
|
66
|
-
To have a file structure
|
89
|
+
To have a file structure Zeitwerk can work with, just name files and directories after the name of the classes and modules they define:
|
67
90
|
|
68
91
|
```
|
69
92
|
lib/my_gem.rb -> MyGem
|
@@ -79,7 +102,7 @@ loader.push_dir(Rails.root.join("app/models"))
|
|
79
102
|
loader.push_dir(Rails.root.join("app/controllers"))
|
80
103
|
```
|
81
104
|
|
82
|
-
|
105
|
+
Zeitwerk understands that their respective files and subdirectories belong to the root namespace:
|
83
106
|
|
84
107
|
```
|
85
108
|
app/models/user.rb -> User
|
@@ -98,85 +121,144 @@ app/controllers/admin/users_controller.rb -> Admin::UsersController
|
|
98
121
|
|
99
122
|
### Explicit namespaces
|
100
123
|
|
101
|
-
Classes and modules that act as namespaces can also be
|
124
|
+
Classes and modules that act as namespaces can also be explicitly defined, though. For instance, consider
|
102
125
|
|
103
126
|
```
|
104
|
-
app/models/hotel.rb
|
105
|
-
app/models/hotel/pricing -> Hotel::Pricing
|
127
|
+
app/models/hotel.rb -> Hotel
|
128
|
+
app/models/hotel/pricing.rb -> Hotel::Pricing
|
106
129
|
```
|
107
130
|
|
108
|
-
Zeitwerk does not autovivify a `Hotel` module in that case. The file `app/models/hotel.rb`
|
131
|
+
Zeitwerk does not autovivify a `Hotel` module in that case. The file `app/models/hotel.rb` explicitly defines `Hotel` and Zeitwerk loads it as needed before going for `Hotel::Pricing`.
|
109
132
|
|
110
|
-
|
133
|
+
### Nested root directories
|
111
134
|
|
135
|
+
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.
|
112
136
|
|
137
|
+
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:
|
113
138
|
|
114
|
-
|
139
|
+
```
|
140
|
+
app/models/concerns/geolocatable.rb
|
141
|
+
```
|
115
142
|
|
116
|
-
|
143
|
+
should define `Geolocatable`, not `Concerns::Geolocatable`.
|
117
144
|
|
118
|
-
##
|
145
|
+
## Usage
|
119
146
|
|
120
|
-
|
147
|
+
### Setup
|
148
|
+
|
149
|
+
Loaders are ready to load code right after calling `setup` on them:
|
121
150
|
|
122
151
|
```ruby
|
123
|
-
|
152
|
+
loader.setup
|
153
|
+
```
|
124
154
|
|
125
|
-
|
126
|
-
Zeitwerk::Loader.for_gem.setup
|
155
|
+
Customization should generally be done before that call. In particular, in the generic interface you may set the root directories from which you want to load files:
|
127
156
|
|
128
|
-
|
129
|
-
|
130
|
-
|
157
|
+
```ruby
|
158
|
+
loader.push_dir(...)
|
159
|
+
loader.push_dir(...)
|
160
|
+
loader.setup
|
131
161
|
```
|
132
162
|
|
133
|
-
The loader of
|
163
|
+
The loader returned by `Zeitwerk::Loader.for_gem` has the directory of the caller pushed, normally that is the absolute path of `lib`. In that sense, `for_gem` can be used also by projects with a gem structure, even if they are not technically gems. That is, you don't need a gemspec or anything.
|
134
164
|
|
135
|
-
|
165
|
+
### Reloading
|
136
166
|
|
137
|
-
|
167
|
+
In order to reload code:
|
138
168
|
|
139
169
|
```ruby
|
140
|
-
|
170
|
+
loader.reload
|
171
|
+
```
|
141
172
|
|
142
|
-
|
143
|
-
|
173
|
+
Generally speaking, reloading is useful for services, servers, web applications, etc. Gems that implement regular libraries, so to speak, won't normally have a use case for reloading.
|
174
|
+
|
175
|
+
It is important to highlight that this is and instance method. Therefore, reloading the code of a project managed by a particular loader does _not_ reload the code of other gems using Zeitwerk at all.
|
176
|
+
|
177
|
+
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.
|
178
|
+
|
179
|
+
### Eager loading
|
180
|
+
|
181
|
+
Zeitwerk instances are able to eager load their managed files:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
loader.eager_load
|
185
|
+
```
|
186
|
+
|
187
|
+
You can opt-out of eager loading individual files or directories:
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
db_adapters = File.expand_path("my_gem/db_adapters", __dir__)
|
191
|
+
cache_adapters = File.expand_path("my_gem/cache_adapters", __dir__)
|
192
|
+
loader.do_not_eager_load(db_adapters, cache_adapters)
|
144
193
|
loader.setup
|
194
|
+
loader.eager_load # won't go into the directories with db/cache adapters
|
145
195
|
```
|
146
196
|
|
147
|
-
|
197
|
+
Files and directories excluded from eager loading can still be loaded on demand, so an idiom like this is possible:
|
148
198
|
|
149
|
-
|
199
|
+
```ruby
|
200
|
+
db_adapter = Object.const_get("MyGem::DbAdapters::#{config[:db_adapter]}")
|
201
|
+
```
|
202
|
+
|
203
|
+
Please check `Zeitwerk::Loader#ignore` if you want files or directories to not be even autoloadable.
|
204
|
+
|
205
|
+
If you want to eager load yourself and all dependencies using Zeitwerk, you can broadcast the `eager_load` call to all instances:
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
Zeitwerk::Loader.eager_load_all
|
209
|
+
```
|
210
|
+
|
211
|
+
In that case, exclusions are per autoloader, and so will apply to each of them accordingly.
|
212
|
+
|
213
|
+
This may be handy in top-level services, like web applications.
|
214
|
+
|
215
|
+
### Preloading
|
216
|
+
|
217
|
+
Zeitwerk instances are able to preload files and directories.
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
loader.preload("app/models/videogame.rb")
|
221
|
+
loader.preload("app/models/book.rb")
|
222
|
+
```
|
223
|
+
|
224
|
+
The example above depicts several calls are supported, but `preload` accepts multiple arguments and arrays of strings as well.
|
225
|
+
|
226
|
+
The call can happen after `setup` (preloads on the spot), or before `setup` (executes during setup).
|
227
|
+
|
228
|
+
If you're using reloading, preloads run on each reload too.
|
150
229
|
|
151
|
-
|
230
|
+
This is a feature specifically thought for STIs in Rails, preloading the leafs of a STI tree ensures all classes are known when doing a query.
|
231
|
+
|
232
|
+
### Inflection
|
152
233
|
|
153
234
|
Each individual loader needs an inflector to figure out which constant path would a given file or directory map to. Zeitwerk ships with two basic inflectors.
|
154
235
|
|
155
|
-
|
236
|
+
#### Zeitwerk::Inflector
|
156
237
|
|
157
|
-
This is a
|
238
|
+
This is a very basic inflector that converts snake case to camel case:
|
158
239
|
|
159
240
|
```
|
160
241
|
user -> User
|
161
242
|
users_controller -> UsersController
|
162
243
|
html_parser -> HtmlParser
|
163
|
-
HTML_parser -> HTMLParser
|
164
244
|
```
|
165
245
|
|
246
|
+
There are no inflection rules or global configuration that can affect this inflector. It is deterministic.
|
247
|
+
|
166
248
|
This is the default inflector.
|
167
249
|
|
168
|
-
|
250
|
+
#### Zeitwerk::GemInflector
|
169
251
|
|
170
252
|
The loader instantiated behind the scenes by `Zeitwerk::Loader.for_gem` gets assigned by default an inflector that is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`.
|
171
253
|
|
172
|
-
|
254
|
+
#### Custom inflector
|
173
255
|
|
174
256
|
The inflectors that ship with Zeitwerk are deterministic and simple. But you can configure your own:
|
175
257
|
|
176
258
|
```ruby
|
177
259
|
# frozen_string_literal: true
|
178
260
|
|
179
|
-
class MyInflector < Zeitwerk::Inflector
|
261
|
+
class MyInflector < Zeitwerk::Inflector # or Zeitwerk::GemInflector
|
180
262
|
def camelize(basename, _abspath)
|
181
263
|
case basename
|
182
264
|
when "api"
|
@@ -197,113 +279,44 @@ The second argument, `abspath`, is a string with the absolute path to the file o
|
|
197
279
|
Then, assign the inflector before calling `setup`:
|
198
280
|
|
199
281
|
```
|
200
|
-
loader = Zeitwerk::Loader.new
|
201
282
|
loader.inflector = MyInflector.new
|
202
|
-
loader.setup
|
203
283
|
```
|
204
284
|
|
205
|
-
|
285
|
+
This needs to be assigned before the call to `setup`.
|
206
286
|
|
207
|
-
|
287
|
+
### Logging
|
208
288
|
|
209
|
-
|
289
|
+
Zeitwerk is silent by default, but you can configure a callable as logger:
|
210
290
|
|
211
291
|
```ruby
|
212
|
-
require "zeitwerk"
|
213
|
-
loader = Zeitwerk::Loader.for_gem
|
214
292
|
loader.logger = method(:puts)
|
215
|
-
loader.setup
|
216
293
|
```
|
217
294
|
|
218
|
-
If there is a logger configured, the
|
295
|
+
If there is a logger configured, the loader is going to print traces when autoloads are set, files loaded, and modules autovivified.
|
219
296
|
|
220
|
-
If your project has namespaces, you'll notice in the traces Zeitwerk sets autoloads for _directories_. That's a technique used to be able to
|
297
|
+
If your project has namespaces, you'll notice in the traces Zeitwerk sets autoloads for _directories_. That's a technique used to be able to descend into subdirectories on demand, avoiding that way unnecessary tree walks.
|
221
298
|
|
222
|
-
|
299
|
+
### Ignoring parts of the project
|
223
300
|
|
224
|
-
|
301
|
+
Sometimes it might be convenient to tell Zeitwerk to completely ignore some particular file or directory. For example, let's suppose that your gem decorates something in `Kernel`:
|
225
302
|
|
226
303
|
```ruby
|
227
|
-
|
228
|
-
loader.setup
|
229
|
-
```
|
230
|
-
|
231
|
-
It is important to highlight that these are instance methods. Therefore, reloading the project managed by your autoloader does _not_ reload the code of other gems using Zeitwerk.
|
304
|
+
# lib/my_gem/core_ext/kernel.rb
|
232
305
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
Zeitwerk instances are able to eager load their managed files:
|
238
|
-
|
239
|
-
```ruby
|
240
|
-
loader.eager_load
|
241
|
-
```
|
242
|
-
|
243
|
-
You can opt-out of eager loading individual files or directories:
|
244
|
-
|
245
|
-
```ruby
|
246
|
-
require "zeitwerk"
|
247
|
-
loader = Zeitwerk::Loader.for_gem
|
248
|
-
|
249
|
-
db_adapters = File.expand_path("my_gem/db_adapters", __dir__)
|
250
|
-
cache_adapters = File.expand_path("my_gem/cache_adapters", __dir__)
|
251
|
-
loader.do_not_eager_load(db_adapters, cache_adapters)
|
252
|
-
loader.eager_load # won't go into the directories with db/cache adapters
|
253
|
-
```
|
254
|
-
|
255
|
-
Files and directories excluded from eager loading can still be autoloaded, so a technique like this is possible:
|
256
|
-
|
257
|
-
```ruby
|
258
|
-
db_adapter = Object.const_get("MyGem::DbAdapters::#{config[:db_adapter]}")
|
306
|
+
Kernel.module_eval do
|
307
|
+
# ...
|
308
|
+
end
|
259
309
|
```
|
260
310
|
|
261
|
-
|
311
|
+
That file does not follow the conventions and you need to tell Zeitwerk:
|
262
312
|
|
263
313
|
```ruby
|
264
|
-
|
265
|
-
loader
|
266
|
-
loader.do_not_eager_load(__dir__)
|
314
|
+
kernel_ext = File.expand_path("my_gem/core_ext/kernel.rb", __dir__)
|
315
|
+
loader.ignore(kernel_ext)
|
267
316
|
loader.setup
|
268
317
|
```
|
269
318
|
|
270
|
-
|
271
|
-
|
272
|
-
```ruby
|
273
|
-
Zeitwerk::Loader.eager_load_all
|
274
|
-
```
|
275
|
-
|
276
|
-
In that case, exclusions are per autoloader, and so will apply to each of them accordingly.
|
277
|
-
|
278
|
-
This may be handy in top-level services, like web applications.
|
279
|
-
|
280
|
-
## Preloading
|
281
|
-
|
282
|
-
Zeitwerk instances are able to preload files and directories.
|
283
|
-
|
284
|
-
```ruby
|
285
|
-
loader.preload("app/models/videogame.rb")
|
286
|
-
loader.preload("app/models/book.rb")
|
287
|
-
```
|
288
|
-
|
289
|
-
The example above depicts several calls are supported, but `preload` accepts multiple arguments and arrays of strings as well.
|
290
|
-
|
291
|
-
The call can happen after `setup` (preloads on the spot), or before `setup` (executes during setup).
|
292
|
-
|
293
|
-
If you're using reloading, preloads run on each reload too.
|
294
|
-
|
295
|
-
This is a feature specifically thought for STIs in Rails, preloading the leafs of an STI tree ensures all classes are known when doing a query.
|
296
|
-
|
297
|
-
Instead of preloading, gems can issue regular requires in the main file:
|
298
|
-
|
299
|
-
```ruby
|
300
|
-
require "zeitwerk"
|
301
|
-
Zeitwerk::Loader.for_gem.setup
|
302
|
-
|
303
|
-
module MyGem
|
304
|
-
require "preload_me"
|
305
|
-
end
|
306
|
-
```
|
319
|
+
You can pass several arguments to this method, also an array of strings. And you can call `ignore` multiple times too.
|
307
320
|
|
308
321
|
## Supported Ruby versions
|
309
322
|
|
@@ -2,17 +2,18 @@
|
|
2
2
|
|
3
3
|
module Zeitwerk
|
4
4
|
class GemInflector < Inflector
|
5
|
-
# @param
|
5
|
+
# @param root_file [String]
|
6
6
|
def initialize(root_file)
|
7
|
-
namespace
|
8
|
-
|
7
|
+
namespace = File.basename(root_file, ".rb")
|
8
|
+
lib_dir = File.dirname(root_file)
|
9
|
+
@version_file = File.join(lib_dir, namespace, "version.rb")
|
9
10
|
end
|
10
11
|
|
11
12
|
# @param basename [String]
|
12
13
|
# @param abspath [String]
|
13
14
|
# @return [String]
|
14
15
|
def camelize(basename, abspath)
|
15
|
-
basename == "version" && abspath
|
16
|
+
(basename == "version" && abspath == @version_file) ? "VERSION" : super
|
16
17
|
end
|
17
18
|
end
|
18
19
|
end
|
data/lib/zeitwerk/inflector.rb
CHANGED
@@ -2,19 +2,17 @@
|
|
2
2
|
|
3
3
|
module Zeitwerk
|
4
4
|
class Inflector # :nodoc:
|
5
|
-
#
|
6
|
-
# expected to be defined in such file or directory.
|
5
|
+
# Very basic snake case -> camel case conversion.
|
7
6
|
#
|
8
7
|
# Zeitwerk::Inflector.camelize("post", ...) # => "Post"
|
9
8
|
# Zeitwerk::Inflector.camelize("users_controller", ...) # => "UsersController"
|
10
9
|
# Zeitwerk::Inflector.camelize("api", ...) # => "Api"
|
11
|
-
# Zeitwerk::Inflector.camelize("HTML", ...) # => "HTML"
|
12
10
|
#
|
13
11
|
# @param basename [String]
|
14
12
|
# @param _abspath [String]
|
15
13
|
# @return [String]
|
16
14
|
def camelize(basename, _abspath)
|
17
|
-
basename.
|
15
|
+
basename.split('_').map(&:capitalize).join
|
18
16
|
end
|
19
17
|
end
|
20
18
|
end
|
data/lib/zeitwerk/loader.rb
CHANGED
@@ -4,20 +4,10 @@ require "set"
|
|
4
4
|
|
5
5
|
module Zeitwerk
|
6
6
|
class Loader
|
7
|
-
# Object used to infer the constant name from a basename without extension.
|
8
|
-
# Default is an instance of Zeitwerk::Inflector.
|
9
|
-
#
|
10
|
-
# inflector.camelize("users_controller", realname) # => "UsersController"
|
11
|
-
#
|
12
|
-
# For context, it receives also the absolute path to the file or directory
|
13
|
-
# name.
|
14
|
-
#
|
15
7
|
# @return [#camelize]
|
16
8
|
attr_accessor :inflector
|
17
9
|
|
18
|
-
#
|
19
|
-
#
|
20
|
-
# @return [nil, #call]
|
10
|
+
# @return [#call, nil]
|
21
11
|
attr_accessor :logger
|
22
12
|
|
23
13
|
# Absolute paths of directories from which you want to load constants. This
|
@@ -26,11 +16,9 @@ module Zeitwerk
|
|
26
16
|
# Stored in a hash to preserve order, easily handle duplicates, and also be
|
27
17
|
# able to have a fast lookup, needed for detecting nested paths.
|
28
18
|
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
# ...
|
33
|
-
# }
|
19
|
+
# "/Users/fxn/blog/app/assets" => true,
|
20
|
+
# "/Users/fxn/blog/app/channels" => true,
|
21
|
+
# ...
|
34
22
|
#
|
35
23
|
# @private
|
36
24
|
# @return [{String => true}]
|
@@ -51,8 +39,9 @@ module Zeitwerk
|
|
51
39
|
# Maps real absolute paths for which an autoload has been set to their
|
52
40
|
# corresponding parent class or module and constant name.
|
53
41
|
#
|
54
|
-
# "/Users/fxn/blog/app/models/user.rb" => [Object, "User"]
|
42
|
+
# "/Users/fxn/blog/app/models/user.rb" => [Object, "User"],
|
55
43
|
# "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, "Pricing"]
|
44
|
+
# ...
|
56
45
|
#
|
57
46
|
# @private
|
58
47
|
# @return [{String => (Module, String)}]
|
@@ -84,6 +73,10 @@ module Zeitwerk
|
|
84
73
|
# @return [Mutex]
|
85
74
|
attr_reader :mutex
|
86
75
|
|
76
|
+
# This tracer listens to `:class` events, and it is used to support explicit
|
77
|
+
# namespaces. Benchmarks have shown the tracer does not impact performance
|
78
|
+
# in any measurable way.
|
79
|
+
#
|
87
80
|
# @private
|
88
81
|
# @return [TracePoint]
|
89
82
|
attr_reader :tracer
|
@@ -92,17 +85,16 @@ module Zeitwerk
|
|
92
85
|
self.inflector = Inflector.new
|
93
86
|
|
94
87
|
@dirs = {}
|
95
|
-
@autoloads = {}
|
96
88
|
@preloads = []
|
97
|
-
@lazy_subdirs = {}
|
98
89
|
@ignored = Set.new
|
90
|
+
@autoloads = {}
|
91
|
+
@lazy_subdirs = {}
|
99
92
|
@eager_load_exclusions = Set.new
|
100
|
-
@mutex = Mutex.new
|
101
|
-
@setup = false
|
102
|
-
@eager_loaded = false
|
103
93
|
|
104
|
-
|
105
|
-
|
94
|
+
@mutex = Mutex.new
|
95
|
+
@setup = false
|
96
|
+
@eager_loaded = false
|
97
|
+
|
106
98
|
@tracer = TracePoint.trace(:class) do |tp|
|
107
99
|
unless lazy_subdirs.empty? # do not even compute the hash key if not needed
|
108
100
|
if subdirs = lazy_subdirs.delete(tp.self.name)
|
@@ -148,7 +140,7 @@ module Zeitwerk
|
|
148
140
|
end
|
149
141
|
end
|
150
142
|
|
151
|
-
# Files or directories to be totally ignored
|
143
|
+
# Files or directories to be totally ignored.
|
152
144
|
#
|
153
145
|
# @param paths [<String, Pathname, <String, Pathname>>]
|
154
146
|
# @return [void]
|
@@ -156,11 +148,7 @@ module Zeitwerk
|
|
156
148
|
mutex.synchronize { ignored.merge(expand_paths(paths)) }
|
157
149
|
end
|
158
150
|
|
159
|
-
# Sets autoloads in the root namespace and preloads files if any.
|
160
|
-
#
|
161
|
-
# Once started, this instance is ready to trace loaded constants if logging
|
162
|
-
# is enabled, to autovivify modules on demand, and to descend further
|
163
|
-
# directory levels to lazily set more autoloads.
|
151
|
+
# Sets autoloads in the root namespace and preloads files, if any.
|
164
152
|
#
|
165
153
|
# @return [void]
|
166
154
|
def setup
|
@@ -181,6 +169,7 @@ module Zeitwerk
|
|
181
169
|
# else, they are eligible for garbage collection, which would effectively
|
182
170
|
# unload them.
|
183
171
|
#
|
172
|
+
# @private
|
184
173
|
# @return [void]
|
185
174
|
def unload
|
186
175
|
mutex.synchronize do
|
@@ -204,7 +193,11 @@ module Zeitwerk
|
|
204
193
|
end
|
205
194
|
end
|
206
195
|
|
207
|
-
#
|
196
|
+
# Unloads all loaded code, and calls setup again so that the loader is able
|
197
|
+
# to pick any changes in the file system.
|
198
|
+
#
|
199
|
+
# This method is not thread-safe, please see how this can be achieved by
|
200
|
+
# client code in the README of the project.
|
208
201
|
#
|
209
202
|
# @return [void]
|
210
203
|
def reload
|
@@ -212,8 +205,10 @@ module Zeitwerk
|
|
212
205
|
setup
|
213
206
|
end
|
214
207
|
|
215
|
-
# Eager loads in the root directories, recursively. Files do not
|
216
|
-
# in `$LOAD_PATH`, absolute file names are used.
|
208
|
+
# Eager loads all files in the root directories, recursively. Files do not
|
209
|
+
# need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
|
210
|
+
# are not eager loaded. You can opt-out specifically in specific files and
|
211
|
+
# directories with `do_not_eager_load`.
|
217
212
|
#
|
218
213
|
# @return [void]
|
219
214
|
def eager_load
|
@@ -228,7 +223,8 @@ module Zeitwerk
|
|
228
223
|
end
|
229
224
|
end
|
230
225
|
|
231
|
-
# Let eager load ignore the given files or directories.
|
226
|
+
# Let eager load ignore the given files or directories. The constants
|
227
|
+
# defined in those files are still autoloadable.
|
232
228
|
#
|
233
229
|
# @param paths [<String, Pathname, <String, Pathname>>]
|
234
230
|
# @return [void]
|
@@ -236,7 +232,7 @@ module Zeitwerk
|
|
236
232
|
mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
|
237
233
|
end
|
238
234
|
|
239
|
-
# --- Class methods
|
235
|
+
# --- Class methods ---------------------------------------------------------------------------
|
240
236
|
|
241
237
|
# This is a shortcut for
|
242
238
|
#
|
@@ -257,14 +253,14 @@ module Zeitwerk
|
|
257
253
|
Registry.loader_for_gem(called_from)
|
258
254
|
end
|
259
255
|
|
260
|
-
# Broadcasts `eager_load` to all
|
256
|
+
# Broadcasts `eager_load` to all loaders.
|
261
257
|
#
|
262
258
|
# @return [void]
|
263
259
|
def self.eager_load_all
|
264
260
|
Registry.loaders.each(&:eager_load)
|
265
261
|
end
|
266
262
|
|
267
|
-
# ---
|
263
|
+
# --- Callbacks -------------------------------------------------------------------------------
|
268
264
|
|
269
265
|
# Callback invoked from Kernel when a managed file is loaded.
|
270
266
|
#
|
@@ -293,9 +289,8 @@ module Zeitwerk
|
|
293
289
|
end
|
294
290
|
end
|
295
291
|
|
296
|
-
private #
|
292
|
+
private # -------------------------------------------------------------------------------------
|
297
293
|
|
298
|
-
# @private
|
299
294
|
# @return [<String>]
|
300
295
|
def actual_dirs
|
301
296
|
dirs.each_key.reject { |dir| ignored.member?(dir) }
|
@@ -303,6 +298,7 @@ module Zeitwerk
|
|
303
298
|
|
304
299
|
# @param dir [String]
|
305
300
|
# @param parent [Module]
|
301
|
+
# @return [void]
|
306
302
|
def set_autoloads_in_dir(dir, parent)
|
307
303
|
each_abspath(dir) do |abspath|
|
308
304
|
cname = inflector.camelize(File.basename(abspath, ".rb"), abspath)
|
@@ -320,16 +316,16 @@ module Zeitwerk
|
|
320
316
|
end
|
321
317
|
end
|
322
318
|
|
323
|
-
# @param subdir [String]
|
324
319
|
# @param parent [Module]
|
325
320
|
# @paran cname [String]
|
321
|
+
# @param subdir [String]
|
326
322
|
# @return [void]
|
327
323
|
def autoload_subdir(parent, cname, subdir)
|
328
324
|
if autoload_for?(parent, cname)
|
329
|
-
# If there is already an autoload for this cname, maybe there are
|
330
|
-
# directories defining the namespace, or the cname is going to
|
331
|
-
# in a file (
|
332
|
-
# another autoload, the existing one is fine.
|
325
|
+
# If there is already an autoload for this cname, maybe there are
|
326
|
+
# multiple directories defining the namespace, or the cname is going to
|
327
|
+
# be defined in a file (explicit namespace). In either case, we do not
|
328
|
+
# need to issue another autoload, the existing one is fine.
|
333
329
|
(lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
|
334
330
|
elsif !parent.const_defined?(cname, false)
|
335
331
|
# First time we find this namespace, set an autoload for it.
|
@@ -342,9 +338,9 @@ module Zeitwerk
|
|
342
338
|
end
|
343
339
|
end
|
344
340
|
|
345
|
-
# @param file [String]
|
346
341
|
# @param parent [Module]
|
347
342
|
# @paran cname [String]
|
343
|
+
# @param file [String]
|
348
344
|
# @return [void]
|
349
345
|
def autoload_file(parent, cname, file)
|
350
346
|
if autoload_path = autoload_for?(parent, cname)
|
@@ -390,12 +386,13 @@ module Zeitwerk
|
|
390
386
|
|
391
387
|
# @param parent [Module]
|
392
388
|
# @param cname [String]
|
393
|
-
# @return [
|
389
|
+
# @return [String, nil]
|
394
390
|
def autoload_for?(parent, cname)
|
395
391
|
parent.autoload?(cname) || Registry.inception?(cpath(parent, cname))
|
396
392
|
end
|
397
393
|
|
398
394
|
# @param dir [String]
|
395
|
+
# @return [void]
|
399
396
|
def eager_load_dir(dir)
|
400
397
|
each_abspath(dir) do |abspath|
|
401
398
|
next if eager_load_exclusions.member?(abspath)
|
@@ -445,7 +442,7 @@ module Zeitwerk
|
|
445
442
|
# @param cname [String]
|
446
443
|
# @return [String]
|
447
444
|
def cpath(parent, cname)
|
448
|
-
parent.equal?(Object) ? cname : "#{parent}::#{cname}"
|
445
|
+
parent.equal?(Object) ? cname : "#{parent.name}::#{cname}"
|
449
446
|
end
|
450
447
|
|
451
448
|
# @param dir [String]
|
data/lib/zeitwerk/registry.rb
CHANGED
@@ -19,17 +19,14 @@ module Zeitwerk
|
|
19
19
|
|
20
20
|
# Maps real paths to the loaders responsible for them.
|
21
21
|
#
|
22
|
-
# This information is used by our decorated Kernel#require to be able to
|
23
|
-
# invoke callbacks.
|
22
|
+
# This information is used by our decorated `Kernel#require` to be able to
|
23
|
+
# invoke callbacks and autovivify modules.
|
24
24
|
#
|
25
25
|
# @private
|
26
26
|
# @return [{String => Zeitwerk::Loader}]
|
27
27
|
attr_reader :autoloads
|
28
28
|
|
29
|
-
# This hash table addresses an edge case in which an autoload is ignored
|
30
|
-
#
|
31
|
-
# Object.autoload(:MyGem, path)
|
32
|
-
# Object.autoload?(:MyGem) # => nil (!!!!)
|
29
|
+
# This hash table addresses an edge case in which an autoload is ignored.
|
33
30
|
#
|
34
31
|
# For example, let's suppose we want to autoload in a gem like this:
|
35
32
|
#
|
@@ -41,16 +38,19 @@ module Zeitwerk
|
|
41
38
|
# module MyGem
|
42
39
|
# end
|
43
40
|
#
|
44
|
-
# if you require "my_gem", this happens while setting
|
41
|
+
# if you require "my_gem", as Bundler would do, this happens while setting
|
42
|
+
# up autoloads:
|
45
43
|
#
|
46
|
-
# 1. Object.autoload?(:MyGem) returns `nil
|
44
|
+
# 1. Object.autoload?(:MyGem) returns `nil` because the autoload for
|
45
|
+
# the constant is issued by Zeitwerk while the same file is being
|
46
|
+
# required.
|
47
47
|
# 2. The constant `MyGem` is undefined while setup runs.
|
48
48
|
#
|
49
49
|
# Therefore, a directory `lib/my_gem` would autovivify a module according to
|
50
50
|
# the existing information. But that would be wrong.
|
51
51
|
#
|
52
52
|
# To overcome this fundamental limitation, we keep track of the constant
|
53
|
-
# paths that are in this situation ---in the example above "MyGem"
|
53
|
+
# paths that are in this situation ---in the example above, "MyGem"--- and
|
54
54
|
# take this collection into account for the autovivification logic.
|
55
55
|
#
|
56
56
|
# Note that you cannot generally address this by moving the setup code
|
@@ -61,16 +61,6 @@ module Zeitwerk
|
|
61
61
|
# include MyConcern
|
62
62
|
# end
|
63
63
|
#
|
64
|
-
# and it would be inelegant to ask users to split that into
|
65
|
-
#
|
66
|
-
# # NO WAY WE ARE GOING TO ASK PEOPLE TO DO THIS.
|
67
|
-
# module MyGem
|
68
|
-
# end
|
69
|
-
# loader.setup
|
70
|
-
# module MyGem
|
71
|
-
# include MyConcern
|
72
|
-
# end
|
73
|
-
#
|
74
64
|
# @private
|
75
65
|
# @return [{String => (String, Zeitwerk::Loader)}]
|
76
66
|
attr_reader :inceptions
|
@@ -79,12 +69,13 @@ module Zeitwerk
|
|
79
69
|
#
|
80
70
|
# @private
|
81
71
|
# @param loader [Zeitwerk::Loader]
|
72
|
+
# @return [void]
|
82
73
|
def register_loader(loader)
|
83
74
|
loaders << loader
|
84
75
|
end
|
85
76
|
|
86
|
-
# This method returns always a loader,
|
87
|
-
#
|
77
|
+
# This method returns always a loader, the same instance for the same root
|
78
|
+
# file. That is how Zeitwerk::Loader.for_gem is idempotent.
|
88
79
|
#
|
89
80
|
# @private
|
90
81
|
# @param root_file [String]
|
@@ -101,12 +92,14 @@ module Zeitwerk
|
|
101
92
|
# @private
|
102
93
|
# @param loader [Zeitwerk::Loader]
|
103
94
|
# @param realpath [String]
|
95
|
+
# @return [void]
|
104
96
|
def register_autoload(loader, realpath)
|
105
97
|
autoloads[realpath] = loader
|
106
98
|
end
|
107
99
|
|
108
100
|
# @private
|
109
101
|
# @param realpath [String]
|
102
|
+
# @return [void]
|
110
103
|
def unregister_autoload(realpath)
|
111
104
|
autoloads.delete(realpath)
|
112
105
|
end
|
@@ -115,6 +108,7 @@ module Zeitwerk
|
|
115
108
|
# @param cpath [String]
|
116
109
|
# @param realpath [String]
|
117
110
|
# @param loader [Zeitwerk::Loader]
|
111
|
+
# @return [void]
|
118
112
|
def register_inception(cpath, realpath, loader)
|
119
113
|
inceptions[cpath] = [realpath, loader]
|
120
114
|
end
|
@@ -137,6 +131,7 @@ module Zeitwerk
|
|
137
131
|
|
138
132
|
# @private
|
139
133
|
# @param loader [Zeitwerk::Loader]
|
134
|
+
# @return [void]
|
140
135
|
def on_unload(loader)
|
141
136
|
autoloads.delete_if { |_path, object| object == loader }
|
142
137
|
inceptions.delete_if { |_cpath, (_path, object)| object == loader }
|
data/lib/zeitwerk/version.rb
CHANGED