zeitwerk 1.0.0.alpha → 1.0.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 +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
|
[![Build Status](https://travis-ci.com/fxn/zeitwerk.svg?branch=master)](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