zeitwerk 1.0.0.alpha
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 +7 -0
- data/README.md +326 -0
- data/lib/zeitwerk.rb +9 -0
- data/lib/zeitwerk/gem_inflector.rb +18 -0
- data/lib/zeitwerk/inflector.rb +20 -0
- data/lib/zeitwerk/kernel.rb +33 -0
- data/lib/zeitwerk/loader.rb +480 -0
- data/lib/zeitwerk/registry.rb +151 -0
- data/lib/zeitwerk/version.rb +5 -0
- metadata +54 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b8109591e515376b3aa028f266d2d354be134aa38cc7b062f7130afc1640f91d
|
4
|
+
data.tar.gz: b9c0118d2cbd8aa5dba6bf22be756f3c7ad448c7320958f76e53fc898ad55b76
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 51bc3e2567f0ef5c6199e56799ab0d851ba7006e35ccf3813d4dde949fe44d9af6848ac4da219e359a017275d86b0fae7fc4657660c8a0c0737ffb02ac78f614
|
7
|
+
data.tar.gz: 4d7051b8d27fc70fd45aaff66c78e00881e758b1d7db1c551026233874f837351aaa72336c1b8fa083b6a83fbc6987f16f0687e9fcf92df8a41f36812d5fad2d
|
data/README.md
ADDED
@@ -0,0 +1,326 @@
|
|
1
|
+
# WIP — NOT PUBLISHED — API AND DOCS IN FLUX
|
2
|
+
|
3
|
+
# Zeitwerk
|
4
|
+
|
5
|
+
[](https://travis-ci.com/fxn/zeitwerk)
|
6
|
+
|
7
|
+
## Introduction
|
8
|
+
|
9
|
+
Zeitwerk is an efficient and thread-safe code loader for Ruby.
|
10
|
+
|
11
|
+
Given a conventional file structure, Zeitwerk loads the project 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.
|
12
|
+
|
13
|
+
This feature is efficient, thread-safe, and matches Ruby's semantics for constants.
|
14
|
+
|
15
|
+
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
|
+
|
17
|
+
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
|
+
|
19
|
+
Finally, in some production setups it may be interesting to be able to eager load all code upfront. Zeitwerk is able to do that too.
|
20
|
+
|
21
|
+
## Synopsis
|
22
|
+
|
23
|
+
Main interface for gems:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
# lib/my_gem.rb (main file)
|
27
|
+
|
28
|
+
require "zeitwerk"
|
29
|
+
Zeitwerk::Loader.for_gem.setup
|
30
|
+
|
31
|
+
module MyGem
|
32
|
+
# ...
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
Main generic interface:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
loader = Zeitwerk::Loader.new
|
40
|
+
loader.push_dir(...)
|
41
|
+
loader.setup
|
42
|
+
```
|
43
|
+
|
44
|
+
Zeitwerk is ready to right after the `setup` call.
|
45
|
+
|
46
|
+
Later, you can reload if you want to:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
loader.reload
|
50
|
+
```
|
51
|
+
|
52
|
+
and you can also eager load:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
loader.eager_load
|
56
|
+
```
|
57
|
+
|
58
|
+
Broadcast `eager_load` to all loaders:
|
59
|
+
|
60
|
+
```
|
61
|
+
Zeitwerk::Loader.eager_load_all
|
62
|
+
```
|
63
|
+
|
64
|
+
## Compatible file structure
|
65
|
+
|
66
|
+
To have a file structure compatible with Zeitwerk, just name files and directories after the name of the classes and modules they define:
|
67
|
+
|
68
|
+
```
|
69
|
+
lib/my_gem.rb -> MyGem
|
70
|
+
lib/my_gem/foo.rb -> MyGem::Foo
|
71
|
+
lib/my_gem/bar_baz.rb -> MyGem::BarBaz
|
72
|
+
lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
|
73
|
+
```
|
74
|
+
|
75
|
+
Every directory configured with `push_dir` acts as root namespace. There can be several of them. For example, given
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
loader.push_dir(Rails.root.join("app/models"))
|
79
|
+
loader.push_dir(Rails.root.join("app/controllers"))
|
80
|
+
```
|
81
|
+
|
82
|
+
you get the mappings
|
83
|
+
|
84
|
+
```
|
85
|
+
app/models/user.rb -> User
|
86
|
+
app/controllers/admin/users_controller.rb -> Admin::UsersController
|
87
|
+
```
|
88
|
+
|
89
|
+
### Implicit namespaces
|
90
|
+
|
91
|
+
Directories without a matching Ruby file get modules autovivified automatically by Zeitwerk. For example, in
|
92
|
+
|
93
|
+
```
|
94
|
+
app/controllers/admin/users_controller.rb -> Admin::UsersController
|
95
|
+
```
|
96
|
+
|
97
|
+
`Admin` is autovivified as a module on demand, you do not need to define an `Admin` class or module in an `admin.rb` file explicitly.
|
98
|
+
|
99
|
+
### Explicit namespaces
|
100
|
+
|
101
|
+
Classes and modules that act as namespaces can also be explictly defined, though. For instance, consider
|
102
|
+
|
103
|
+
```
|
104
|
+
app/models/hotel.rb -> Hotel
|
105
|
+
app/models/hotel/pricing -> Hotel::Pricing
|
106
|
+
```
|
107
|
+
|
108
|
+
Zeitwerk does not autovivify a `Hotel` module in that case. The file `app/models/hotel.rb` explictly defines `Hotel` and Zeitwerk loads it as needed before going for `Hotel::Pricing`.
|
109
|
+
|
110
|
+
## Synopsis
|
111
|
+
|
112
|
+
|
113
|
+
|
114
|
+
The autoloading semantics provided by Zeitwerk match Ruby's. Zeitwerk bases autoloading on `Kernel#autoload`, which has builtin support in the interpreter in constant lookup algorithms and related APIs.
|
115
|
+
|
116
|
+
Autoloadable files can be required. The idea is to _not_ use `require` at all with a gem managed by Zeitwerk, but support for this exists in case your users have `require` calls already in place and upgrade, for example.
|
117
|
+
|
118
|
+
## Use case: Gems
|
119
|
+
|
120
|
+
To use Zeitwerk in a gem just throw these couple of lines in the main file:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
# lib/my_gem.rb
|
124
|
+
|
125
|
+
require "zeitwerk"
|
126
|
+
Zeitwerk::Loader.for_gem.setup
|
127
|
+
|
128
|
+
module MyGem
|
129
|
+
# ...
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
The loader of your gem is exclusive, it has its own configuration, inflector, and logger.
|
134
|
+
|
135
|
+
## Use case: Generic interface
|
136
|
+
|
137
|
+
The generic interface to use Zeitwerk in a different context is
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
require "zeitwerk"
|
141
|
+
|
142
|
+
loader = Zeitwerk::Loader.new
|
143
|
+
loader.push_dir("...")
|
144
|
+
loader.setup
|
145
|
+
```
|
146
|
+
|
147
|
+
A loader instantiated that way is independent of other loaders. It has its own configuration, inflector, and logger.
|
148
|
+
|
149
|
+
The variable used to setup the loader can get out of scope. The instance won't be garbage collected because Zeitwerk keeps a registry of them.
|
150
|
+
|
151
|
+
## Inflection
|
152
|
+
|
153
|
+
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
|
+
|
155
|
+
### Zeitwerk::Inflector
|
156
|
+
|
157
|
+
This is a super basic inflector that converts snake case to camel case preserving upper case letters:
|
158
|
+
|
159
|
+
```
|
160
|
+
user -> User
|
161
|
+
users_controller -> UsersController
|
162
|
+
html_parser -> HtmlParser
|
163
|
+
HTML_parser -> HTMLParser
|
164
|
+
```
|
165
|
+
|
166
|
+
This is the default inflector.
|
167
|
+
|
168
|
+
### Zeitwerk::GemInflector
|
169
|
+
|
170
|
+
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
|
+
|
172
|
+
### Custom inflector
|
173
|
+
|
174
|
+
The inflectors that ship with Zeitwerk are deterministic and simple. But you can configure your own:
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
# frozen_string_literal: true
|
178
|
+
|
179
|
+
class MyInflector < Zeitwerk::Inflector
|
180
|
+
def camelize(basename, _abspath)
|
181
|
+
case basename
|
182
|
+
when "api"
|
183
|
+
"API"
|
184
|
+
when "mysql_adapter"
|
185
|
+
"MySQLAdapter"
|
186
|
+
else
|
187
|
+
super
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
The first argument, `basename`, is a string with the basename of the file or directory to be inflected. In the case of a file, without extension. The inflector needs to return this basename inflected. Therefore, a simple constant name without colons.
|
194
|
+
|
195
|
+
The second argument, `abspath`, is a string with the absolute path to the file or directory in case you need it to decide how to inflect the basename.
|
196
|
+
|
197
|
+
Then, assign the inflector before calling `setup`:
|
198
|
+
|
199
|
+
```
|
200
|
+
loader = Zeitwerk::Loader.new
|
201
|
+
loader.inflector = MyInflector.new
|
202
|
+
loader.setup
|
203
|
+
```
|
204
|
+
|
205
|
+
## Logging
|
206
|
+
|
207
|
+
Zeitwerk is silent by default, but you can configure a callable as logger.
|
208
|
+
|
209
|
+
In the case of gems, for example:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
require "zeitwerk"
|
213
|
+
loader = Zeitwerk::Loader.for_gem
|
214
|
+
loader.logger = method(:puts)
|
215
|
+
loader.setup
|
216
|
+
```
|
217
|
+
|
218
|
+
If there is a logger configured, the the loader is going to print traces when autoloads are set, files preloaded, files autoloaded, and modules autovivified from directories.
|
219
|
+
|
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 be lazy and avoid unnecessary tree walks. It sets autoloads one depth level at a time, and only on demand.
|
221
|
+
|
222
|
+
## Reloading
|
223
|
+
|
224
|
+
In order to reload code, unload and setup:
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
loader.unload
|
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.
|
232
|
+
|
233
|
+
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.
|
234
|
+
|
235
|
+
## Eager loading
|
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]}")
|
259
|
+
```
|
260
|
+
|
261
|
+
You can even opt-out from eager loading entirely:
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
require "zeitwerk"
|
265
|
+
loader = Zeitwerk::Loader.for_gem
|
266
|
+
loader.do_not_eager_load(__dir__)
|
267
|
+
loader.setup
|
268
|
+
```
|
269
|
+
|
270
|
+
If you want to eager load yourself and all dependencies using Zeitwerk, you can broadcast the `eager_load` call to all registered instances:
|
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
|
+
```
|
307
|
+
|
308
|
+
## Supported Ruby versions
|
309
|
+
|
310
|
+
Zeitwerk works with MRI 2.4.4 and above.
|
311
|
+
|
312
|
+
## Motivation
|
313
|
+
|
314
|
+
Since `require` has global side-effects, and there is no static way to verify that you have issued the `require` calls for code that your file depends on, in practice it is very easy to forget some. That introduces bugs that depend on the load order. Zeitwerk provides a way to forget about `require` in your own code, just name things following conventions and done.
|
315
|
+
|
316
|
+
On the other hand, autoloading in Rails is based on `const_missing`, which lacks fundamental information like the nesting and the resolution algorithm that was being used. Because of that, Rails autoloading is not able to match Ruby's semantics and that introduces a series of gotchas. The original goal of this project was to bring a better autoloading mechanism for Rails 6.
|
317
|
+
|
318
|
+
## Thanks
|
319
|
+
|
320
|
+
I'd like to thank [@matthewd](https://github.com/matthewd) for the discussions we've had about this topic in the past years, I learned a couple of tricks used in Zeitwerk from him.
|
321
|
+
|
322
|
+
Also would like to thank [@Shopify](https://github.com/Shopify), [@rafaelfranca](https://github.com/rafaelfranca), and [@dylanahsmith](https://github.com/dylanahsmith), for sharing [this PoC](https://github.com/Shopify/autoload_reloader). The technique Zeitwerk uses to support explicit namespaces was copied from that project.
|
323
|
+
|
324
|
+
## License
|
325
|
+
|
326
|
+
Released under the MIT License, Copyright (c) 2019–<i>ω</i> Xavier Noria.
|
data/lib/zeitwerk.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zeitwerk
|
4
|
+
class GemInflector < Inflector
|
5
|
+
# @param gem_entry_point [String]
|
6
|
+
def initialize(root_file)
|
7
|
+
namespace = File.basename(root_file, ".rb")
|
8
|
+
@version_file = File.join(namespace, "version.rb")
|
9
|
+
end
|
10
|
+
|
11
|
+
# @param basename [String]
|
12
|
+
# @param abspath [String]
|
13
|
+
# @return [String]
|
14
|
+
def camelize(basename, abspath)
|
15
|
+
basename == "version" && abspath.end_with?(@version_file) ? "VERSION" : super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zeitwerk
|
4
|
+
class Inflector # :nodoc:
|
5
|
+
# Given a basename without extension, returns the name of the constant
|
6
|
+
# expected to be defined in such file or directory.
|
7
|
+
#
|
8
|
+
# Zeitwerk::Inflector.camelize("post", ...) # => "Post"
|
9
|
+
# Zeitwerk::Inflector.camelize("users_controller", ...) # => "UsersController"
|
10
|
+
# Zeitwerk::Inflector.camelize("api", ...) # => "Api"
|
11
|
+
# Zeitwerk::Inflector.camelize("HTML", ...) # => "HTML"
|
12
|
+
#
|
13
|
+
# @param basename [String]
|
14
|
+
# @param _abspath [String]
|
15
|
+
# @return [String]
|
16
|
+
def camelize(basename, _abspath)
|
17
|
+
basename.gsub(/(?:\A|_)(\w)/) { $1.capitalize }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kernel
|
4
|
+
module_function
|
5
|
+
|
6
|
+
# We cannot decorate with prepend + super because Kernel has already been
|
7
|
+
# included in Object, and changes in ancestors don't get propagated into
|
8
|
+
# already existing ancestor chains.
|
9
|
+
alias_method :zeitwerk_original_require, :require
|
10
|
+
|
11
|
+
# @param path [String]
|
12
|
+
# @return [Boolean]
|
13
|
+
def require(path)
|
14
|
+
if loader = Zeitwerk::Registry.loader_for(path)
|
15
|
+
if path.end_with?(".rb")
|
16
|
+
zeitwerk_original_require(path).tap do |required|
|
17
|
+
loader.on_file_loaded(path) if required
|
18
|
+
end
|
19
|
+
else
|
20
|
+
loader.on_dir_loaded(path)
|
21
|
+
end
|
22
|
+
else
|
23
|
+
zeitwerk_original_require(path).tap do |required|
|
24
|
+
if required
|
25
|
+
realpath = $LOADED_FEATURES.last
|
26
|
+
if loader = Zeitwerk::Registry.loader_for(realpath)
|
27
|
+
loader.on_file_loaded(realpath)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,480 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module Zeitwerk
|
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
|
+
# @return [#camelize]
|
16
|
+
attr_accessor :inflector
|
17
|
+
|
18
|
+
# An optional callable to log when a constant is loaded, `nil` by default.
|
19
|
+
#
|
20
|
+
# @return [nil, #call]
|
21
|
+
attr_accessor :logger
|
22
|
+
|
23
|
+
# Absolute paths of directories from which you want to load constants. This
|
24
|
+
# is a private attribute, client code should use `push_dir`.
|
25
|
+
#
|
26
|
+
# Stored in a hash to preserve order, easily handle duplicates, and also be
|
27
|
+
# able to have a fast lookup, needed for detecting nested paths.
|
28
|
+
#
|
29
|
+
# {
|
30
|
+
# "/Users/fxn/blog/app/assets" => true,
|
31
|
+
# "/Users/fxn/blog/app/channels" => true,
|
32
|
+
# ...
|
33
|
+
# }
|
34
|
+
#
|
35
|
+
# @private
|
36
|
+
# @return [{String => true}]
|
37
|
+
attr_reader :dirs
|
38
|
+
|
39
|
+
# Absolute paths of files or directories that have to be preloaded.
|
40
|
+
#
|
41
|
+
# @private
|
42
|
+
# @return [<String>]
|
43
|
+
attr_reader :preloads
|
44
|
+
|
45
|
+
# Absolute paths of files or directories to be totally ignored.
|
46
|
+
#
|
47
|
+
# @private
|
48
|
+
# @return [String]
|
49
|
+
attr_reader :ignored
|
50
|
+
|
51
|
+
# Maps real absolute paths for which an autoload has been set to their
|
52
|
+
# corresponding parent class or module and constant name.
|
53
|
+
#
|
54
|
+
# "/Users/fxn/blog/app/models/user.rb" => [Object, "User"]
|
55
|
+
# "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, "Pricing"]
|
56
|
+
#
|
57
|
+
# @private
|
58
|
+
# @return [{String => (Module, String)}]
|
59
|
+
attr_reader :autoloads
|
60
|
+
|
61
|
+
# Maps constant paths of namespaces to arrays of corresponding directories.
|
62
|
+
#
|
63
|
+
# For example, given this mapping:
|
64
|
+
#
|
65
|
+
# "Admin" => [
|
66
|
+
# "/Users/fxn/blog/app/controllers/admin",
|
67
|
+
# "/Users/fxn/blog/app/models/admin",
|
68
|
+
# ...
|
69
|
+
# ]
|
70
|
+
#
|
71
|
+
# when `Admin` gets defined we know that it plays the role of a namespace and
|
72
|
+
# that its children are spread over those directories. We'll visit them to set
|
73
|
+
# up the corresponding autoloads.
|
74
|
+
#
|
75
|
+
# @private
|
76
|
+
# @return [{String => <String>}]
|
77
|
+
attr_reader :lazy_subdirs
|
78
|
+
|
79
|
+
# @private
|
80
|
+
# @return [Set]
|
81
|
+
attr_reader :eager_load_exclusions
|
82
|
+
|
83
|
+
# @private
|
84
|
+
# @return [Mutex]
|
85
|
+
attr_reader :mutex
|
86
|
+
|
87
|
+
# @private
|
88
|
+
# @return [TracePoint]
|
89
|
+
attr_reader :tracer
|
90
|
+
|
91
|
+
def initialize
|
92
|
+
self.inflector = Inflector.new
|
93
|
+
|
94
|
+
@dirs = {}
|
95
|
+
@autoloads = {}
|
96
|
+
@preloads = []
|
97
|
+
@lazy_subdirs = {}
|
98
|
+
@ignored = Set.new
|
99
|
+
@eager_load_exclusions = Set.new
|
100
|
+
@mutex = Mutex.new
|
101
|
+
@setup = false
|
102
|
+
@eager_loaded = false
|
103
|
+
|
104
|
+
# We have run several benchmarks that have shown having a trace point for
|
105
|
+
# the :class event does not impact performance in any measurable way.
|
106
|
+
@tracer = TracePoint.trace(:class) do |tp|
|
107
|
+
unless lazy_subdirs.empty? # do not even compute the hash key if not needed
|
108
|
+
if subdirs = lazy_subdirs.delete(tp.self.name)
|
109
|
+
subdirs.each { |subdir| set_autoloads_in_dir(subdir, tp.self) }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
Registry.register_loader(self)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Pushes `paths` to the list of root directories.
|
118
|
+
#
|
119
|
+
# @param path [<String, Pathname>]
|
120
|
+
# @return [void]
|
121
|
+
def push_dir(path)
|
122
|
+
abspath = File.expand_path(path)
|
123
|
+
mutex.synchronize do
|
124
|
+
if dir?(abspath)
|
125
|
+
dirs[abspath] = true
|
126
|
+
else
|
127
|
+
raise ArgumentError, "the root directory #{abspath} does not exist"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Files or directories to be preloaded instead of lazy loaded.
|
133
|
+
#
|
134
|
+
# @param paths [<String, Pathname, <String, Pathname>>]
|
135
|
+
# @return [void]
|
136
|
+
def preload(*paths)
|
137
|
+
mutex.synchronize do
|
138
|
+
expand_paths(paths).each do |abspath|
|
139
|
+
preloads << abspath
|
140
|
+
if @setup
|
141
|
+
if ruby?(abspath)
|
142
|
+
do_preload_file(abspath)
|
143
|
+
elsif dir?(abspath)
|
144
|
+
do_preload_dir(abspath)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Files or directories to be totally ignored by Zeitwerk.
|
152
|
+
#
|
153
|
+
# @param paths [<String, Pathname, <String, Pathname>>]
|
154
|
+
# @return [void]
|
155
|
+
def ignore(*paths)
|
156
|
+
mutex.synchronize { ignored.merge(expand_paths(paths)) }
|
157
|
+
end
|
158
|
+
|
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.
|
164
|
+
#
|
165
|
+
# @return [void]
|
166
|
+
def setup
|
167
|
+
mutex.synchronize do
|
168
|
+
unless @setup
|
169
|
+
actual_dirs.each { |dir| set_autoloads_in_dir(dir, Object) }
|
170
|
+
tracer.enable
|
171
|
+
do_preload
|
172
|
+
@setup = true
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Removes loaded constants and configured autoloads.
|
178
|
+
#
|
179
|
+
# The objects the constants stored are no longer reachable through them. In
|
180
|
+
# addition, since said objects are normally not referenced from anywhere
|
181
|
+
# else, they are eligible for garbage collection, which would effectively
|
182
|
+
# unload them.
|
183
|
+
#
|
184
|
+
# @return [void]
|
185
|
+
def unload
|
186
|
+
mutex.synchronize do
|
187
|
+
autoloads.each do |path, (parent, cname)|
|
188
|
+
# If the constant was loaded, we unload it. Otherwise, this removes
|
189
|
+
# the autoload in the parent, which is something we want to do anyway.
|
190
|
+
parent.send(:remove_const, cname) rescue :user_removed_it_by_hand_that_is_fine
|
191
|
+
|
192
|
+
# Let Kernel#require load the same path later again by removing it
|
193
|
+
# from $LOADED_FEATURES. We check the extension to avoid unnecessary
|
194
|
+
# array lookups, since directories are not stored in $LOADED_FEATURES.
|
195
|
+
$LOADED_FEATURES.delete(path) if ruby?(path)
|
196
|
+
end
|
197
|
+
autoloads.clear
|
198
|
+
lazy_subdirs.clear
|
199
|
+
|
200
|
+
Registry.on_unload(self)
|
201
|
+
|
202
|
+
tracer.disable
|
203
|
+
@setup = false
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Docs pending.
|
208
|
+
#
|
209
|
+
# @return [void]
|
210
|
+
def reload
|
211
|
+
unload
|
212
|
+
setup
|
213
|
+
end
|
214
|
+
|
215
|
+
# Eager loads in the root directories, recursively. Files do not need to be
|
216
|
+
# in `$LOAD_PATH`, absolute file names are used.
|
217
|
+
#
|
218
|
+
# @return [void]
|
219
|
+
def eager_load
|
220
|
+
mutex.synchronize do
|
221
|
+
unless @eager_loaded
|
222
|
+
actual_dirs.each do |dir|
|
223
|
+
eager_load_dir(dir) unless eager_load_exclusions.member?(dir)
|
224
|
+
end
|
225
|
+
tracer.disable if eager_load_exclusions.empty?
|
226
|
+
@eager_loaded = true
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Let eager load ignore the given files or directories.
|
232
|
+
#
|
233
|
+
# @param paths [<String, Pathname, <String, Pathname>>]
|
234
|
+
# @return [void]
|
235
|
+
def do_not_eager_load(*paths)
|
236
|
+
mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
|
237
|
+
end
|
238
|
+
|
239
|
+
# --- Class methods -----------------------------------------------------------------------------
|
240
|
+
|
241
|
+
# This is a shortcut for
|
242
|
+
#
|
243
|
+
# require "zeitwerk"
|
244
|
+
# loader = Zeitwerk::Loader.new
|
245
|
+
# loader.inflector = Zeitwerk::GemInflector.new
|
246
|
+
# loader.push_dir(__dir__)
|
247
|
+
#
|
248
|
+
# except that this method returns the same object in subsequent calls from
|
249
|
+
# the same file, in the unlikely case the gem wants to be able to reload.
|
250
|
+
#
|
251
|
+
# `Zeitwerk::GemInflector` is a subclass of `Zeitwerk::Inflector` that
|
252
|
+
# camelizes "lib/my_gem/version.rb" as "MyGem::VERSION".
|
253
|
+
#
|
254
|
+
# @return [Zeitwerk::Loader]
|
255
|
+
def self.for_gem
|
256
|
+
called_from = caller[0].split(':')[0]
|
257
|
+
Registry.loader_for_gem(called_from)
|
258
|
+
end
|
259
|
+
|
260
|
+
# Broadcasts `eager_load` to all instances.
|
261
|
+
#
|
262
|
+
# @return [void]
|
263
|
+
def self.eager_load_all
|
264
|
+
Registry.loaders.each(&:eager_load)
|
265
|
+
end
|
266
|
+
|
267
|
+
# --- Calbacks ----------------------------------------------------------------------------------
|
268
|
+
|
269
|
+
# Callback invoked from Kernel when a managed file is loaded.
|
270
|
+
#
|
271
|
+
# @private
|
272
|
+
# @param file [String]
|
273
|
+
# @return [void]
|
274
|
+
def on_file_loaded(file)
|
275
|
+
if logger
|
276
|
+
parent, cname = autoloads[file]
|
277
|
+
logger.call("constant #{cpath(parent, cname)} loaded from file #{file}")
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# Callback invoked from Kernel when a managed directory is loaded.
|
282
|
+
#
|
283
|
+
# @private
|
284
|
+
# @param dir [String]
|
285
|
+
# @return [void]
|
286
|
+
def on_dir_loaded(dir)
|
287
|
+
parent, cname = autoloads[dir]
|
288
|
+
autovivified = parent.const_set(cname, Module.new)
|
289
|
+
logger.call("module #{cpath(parent, cname)} autovivified from directory #{dir}") if logger
|
290
|
+
|
291
|
+
if subdirs = lazy_subdirs[cpath(parent, cname)]
|
292
|
+
subdirs.each { |subdir| set_autoloads_in_dir(subdir, autovivified) }
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
private # ---------------------------------------------------------------------------------------
|
297
|
+
|
298
|
+
# @private
|
299
|
+
# @return [<String>]
|
300
|
+
def actual_dirs
|
301
|
+
dirs.each_key.reject { |dir| ignored.member?(dir) }
|
302
|
+
end
|
303
|
+
|
304
|
+
# @param dir [String]
|
305
|
+
# @param parent [Module]
|
306
|
+
def set_autoloads_in_dir(dir, parent)
|
307
|
+
each_abspath(dir) do |abspath|
|
308
|
+
cname = inflector.camelize(File.basename(abspath, ".rb"), abspath)
|
309
|
+
if ruby?(abspath)
|
310
|
+
autoload_file(parent, cname, abspath)
|
311
|
+
elsif dir?(abspath)
|
312
|
+
# In a Rails application, `app/models/concerns` is a subdirectory of
|
313
|
+
# `app/models`, but both of them are root directories.
|
314
|
+
#
|
315
|
+
# To resolve the ambiguity file name -> constant path this introduces,
|
316
|
+
# the `app/models/concerns` directory is totally ignored as a namespace,
|
317
|
+
# it counts only as root. The guard checks that.
|
318
|
+
autoload_subdir(parent, cname, abspath) unless dirs.key?(abspath)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# @param subdir [String]
|
324
|
+
# @param parent [Module]
|
325
|
+
# @paran cname [String]
|
326
|
+
# @return [void]
|
327
|
+
def autoload_subdir(parent, cname, subdir)
|
328
|
+
if autoload_for?(parent, cname)
|
329
|
+
# If there is already an autoload for this cname, maybe there are multiple
|
330
|
+
# directories defining the namespace, or the cname is going to be defined
|
331
|
+
# in a file (not autovivified). In either case, we do not need to issue
|
332
|
+
# another autoload, the existing one is fine.
|
333
|
+
(lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
|
334
|
+
elsif !parent.const_defined?(cname, false)
|
335
|
+
# First time we find this namespace, set an autoload for it.
|
336
|
+
(lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
|
337
|
+
set_autoload(parent, cname, subdir)
|
338
|
+
else
|
339
|
+
# For whatever reason the constant that corresponds to this namespace has
|
340
|
+
# already been defined, we have to recurse.
|
341
|
+
set_autoloads_in_dir(subdir, parent.const_get(cname))
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# @param file [String]
|
346
|
+
# @param parent [Module]
|
347
|
+
# @paran cname [String]
|
348
|
+
# @return [void]
|
349
|
+
def autoload_file(parent, cname, file)
|
350
|
+
if autoload_path = autoload_for?(parent, cname)
|
351
|
+
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
352
|
+
return if ruby?(autoload_path)
|
353
|
+
|
354
|
+
# Override autovivification, we want the namespace to become the
|
355
|
+
# class/module defined in this file.
|
356
|
+
autoloads.delete(autoload_path)
|
357
|
+
Registry.unregister_autoload(autoload_path)
|
358
|
+
set_autoload(parent, cname, file)
|
359
|
+
elsif !parent.const_defined?(cname, false)
|
360
|
+
set_autoload(parent, cname, file)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
# @param parent [Module]
|
365
|
+
# @param cname [String]
|
366
|
+
# @param abspath [String]
|
367
|
+
# @return [void]
|
368
|
+
def set_autoload(parent, cname, abspath)
|
369
|
+
# $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the
|
370
|
+
# real path to be able to delete it from $LOADED_FEATURES on unload, and to
|
371
|
+
# be able to do a lookup later in Kernel#require for manual require calls.
|
372
|
+
realpath = File.realpath(abspath)
|
373
|
+
parent.autoload(cname, realpath)
|
374
|
+
if logger
|
375
|
+
if ruby?(realpath)
|
376
|
+
logger.call("autoload set for #{cpath(parent, cname)}, to be loaded from #{realpath}")
|
377
|
+
else
|
378
|
+
logger.call("autoload set for #{cpath(parent, cname)}, to be autovivified from #{realpath}")
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
autoloads[realpath] = [parent, cname]
|
383
|
+
Registry.register_autoload(self, realpath)
|
384
|
+
|
385
|
+
# See why in the documentation of Zeitwerk::Registry.inceptions.
|
386
|
+
unless parent.autoload?(cname)
|
387
|
+
Registry.register_inception(cpath(parent, cname), realpath, self)
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
# @param parent [Module]
|
392
|
+
# @param cname [String]
|
393
|
+
# @return [nil, String]
|
394
|
+
def autoload_for?(parent, cname)
|
395
|
+
parent.autoload?(cname) || Registry.inception?(cpath(parent, cname))
|
396
|
+
end
|
397
|
+
|
398
|
+
# @param dir [String]
|
399
|
+
def eager_load_dir(dir)
|
400
|
+
each_abspath(dir) do |abspath|
|
401
|
+
next if eager_load_exclusions.member?(abspath)
|
402
|
+
|
403
|
+
if ruby?(abspath)
|
404
|
+
require abspath
|
405
|
+
elsif dir?(abspath)
|
406
|
+
eager_load_dir(abspath)
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
# This method is called this way because I prefer `preload` to be the method
|
412
|
+
# name to configure preloads in the public interface.
|
413
|
+
#
|
414
|
+
# @return [void]
|
415
|
+
def do_preload
|
416
|
+
preloads.each do |abspath|
|
417
|
+
if ruby?(abspath)
|
418
|
+
do_preload_file(abspath)
|
419
|
+
elsif dir?(abspath)
|
420
|
+
do_preload_dir(abspath)
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
# @param dir [String]
|
426
|
+
# @return [void]
|
427
|
+
def do_preload_dir(dir)
|
428
|
+
each_abspath(dir) do |abspath|
|
429
|
+
if ruby?(abspath)
|
430
|
+
do_preload_file(abspath)
|
431
|
+
elsif dir?(abspath)
|
432
|
+
do_preload_dir(abspath)
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
# @param file [String]
|
438
|
+
# @return [Boolean]
|
439
|
+
def do_preload_file(file)
|
440
|
+
logger.call("preloading #{file}") if logger
|
441
|
+
require file
|
442
|
+
end
|
443
|
+
|
444
|
+
# @param parent [Module]
|
445
|
+
# @param cname [String]
|
446
|
+
# @return [String]
|
447
|
+
def cpath(parent, cname)
|
448
|
+
parent.equal?(Object) ? cname : "#{parent}::#{cname}"
|
449
|
+
end
|
450
|
+
|
451
|
+
# @param dir [String]
|
452
|
+
# @yieldparam path [String]
|
453
|
+
# @return [void]
|
454
|
+
def each_abspath(dir)
|
455
|
+
Dir.foreach(dir) do |entry|
|
456
|
+
next if entry.start_with?(".")
|
457
|
+
abspath = File.join(dir, entry)
|
458
|
+
yield abspath unless ignored.member?(abspath)
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
# @param path [String]
|
463
|
+
# @return [Boolean]
|
464
|
+
def ruby?(path)
|
465
|
+
path.end_with?(".rb")
|
466
|
+
end
|
467
|
+
|
468
|
+
# @param path [String]
|
469
|
+
# @return [Boolean]
|
470
|
+
def dir?(path)
|
471
|
+
File.directory?(path)
|
472
|
+
end
|
473
|
+
|
474
|
+
# @param paths [<String, Pathname, <String, Pathname>>]
|
475
|
+
# @return [<String>]
|
476
|
+
def expand_paths(paths)
|
477
|
+
Array(paths).flatten.map { |path| File.expand_path(path) }
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zeitwerk
|
4
|
+
module Registry # :nodoc: all
|
5
|
+
class << self
|
6
|
+
# Keeps track of all loaders. Useful to broadcast messages and to prevent
|
7
|
+
# them from being garbage collected.
|
8
|
+
#
|
9
|
+
# @private
|
10
|
+
# @return [<Zeitwerk::Loader>]
|
11
|
+
attr_reader :loaders
|
12
|
+
|
13
|
+
# Registers loaders created with `for_gem` to make the method idempotent
|
14
|
+
# in case of reload.
|
15
|
+
#
|
16
|
+
# @private
|
17
|
+
# @return [{String => Zeitwerk::Loader}]
|
18
|
+
attr_reader :loaders_managing_gems
|
19
|
+
|
20
|
+
# Maps real paths to the loaders responsible for them.
|
21
|
+
#
|
22
|
+
# This information is used by our decorated Kernel#require to be able to
|
23
|
+
# invoke callbacks.
|
24
|
+
#
|
25
|
+
# @private
|
26
|
+
# @return [{String => Zeitwerk::Loader}]
|
27
|
+
attr_reader :autoloads
|
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 (!!!!)
|
33
|
+
#
|
34
|
+
# For example, let's suppose we want to autoload in a gem like this:
|
35
|
+
#
|
36
|
+
# # lib/my_gem.rb
|
37
|
+
# loader = Zeitwerk::Loader.new
|
38
|
+
# loader.push_dir(__dir__)
|
39
|
+
# loader.setup
|
40
|
+
#
|
41
|
+
# module MyGem
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# if you require "my_gem", this happens while setting up autoloads:
|
45
|
+
#
|
46
|
+
# 1. Object.autoload?(:MyGem) returns `nil`.
|
47
|
+
# 2. The constant `MyGem` is undefined while setup runs.
|
48
|
+
#
|
49
|
+
# Therefore, a directory `lib/my_gem` would autovivify a module according to
|
50
|
+
# the existing information. But that would be wrong.
|
51
|
+
#
|
52
|
+
# To overcome this fundamental limitation, we keep track of the constant
|
53
|
+
# paths that are in this situation ---in the example above "MyGem"---, and
|
54
|
+
# take this collection into account for the autovivification logic.
|
55
|
+
#
|
56
|
+
# Note that you cannot generally address this by moving the setup code
|
57
|
+
# below the constant definition, because we want libraries to be able to
|
58
|
+
# use managed constants in the module body:
|
59
|
+
#
|
60
|
+
# module MyGem
|
61
|
+
# include MyConcern
|
62
|
+
# end
|
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
|
+
# @private
|
75
|
+
# @return [{String => (String, Zeitwerk::Loader)}]
|
76
|
+
attr_reader :inceptions
|
77
|
+
|
78
|
+
# Registers a loader.
|
79
|
+
#
|
80
|
+
# @private
|
81
|
+
# @param loader [Zeitwerk::Loader]
|
82
|
+
def register_loader(loader)
|
83
|
+
loaders << loader
|
84
|
+
end
|
85
|
+
|
86
|
+
# This method returns always a loader, and it returns the same instance
|
87
|
+
# for the same root file. That is how Zeitwerk.start is idempotent.
|
88
|
+
#
|
89
|
+
# @private
|
90
|
+
# @param root_file [String]
|
91
|
+
# @return [Zeitwerk::Loader]
|
92
|
+
def loader_for_gem(root_file)
|
93
|
+
loaders_managing_gems[root_file] ||= begin
|
94
|
+
Loader.new.tap do |loader|
|
95
|
+
loader.inflector = Zeitwerk::GemInflector.new(root_file)
|
96
|
+
loader.push_dir(File.dirname(root_file))
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# @private
|
102
|
+
# @param loader [Zeitwerk::Loader]
|
103
|
+
# @param realpath [String]
|
104
|
+
def register_autoload(loader, realpath)
|
105
|
+
autoloads[realpath] = loader
|
106
|
+
end
|
107
|
+
|
108
|
+
# @private
|
109
|
+
# @param realpath [String]
|
110
|
+
def unregister_autoload(realpath)
|
111
|
+
autoloads.delete(realpath)
|
112
|
+
end
|
113
|
+
|
114
|
+
# @private
|
115
|
+
# @param cpath [String]
|
116
|
+
# @param realpath [String]
|
117
|
+
# @param loader [Zeitwerk::Loader]
|
118
|
+
def register_inception(cpath, realpath, loader)
|
119
|
+
inceptions[cpath] = [realpath, loader]
|
120
|
+
end
|
121
|
+
|
122
|
+
# @private
|
123
|
+
# @param cpath [String]
|
124
|
+
# @return [String, nil]
|
125
|
+
def inception?(cpath)
|
126
|
+
if pair = inceptions[cpath]
|
127
|
+
pair.first
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# @private
|
132
|
+
# @param path [String]
|
133
|
+
# @return [Zeitwerk::Loader, nil]
|
134
|
+
def loader_for(path)
|
135
|
+
autoloads[path]
|
136
|
+
end
|
137
|
+
|
138
|
+
# @private
|
139
|
+
# @param loader [Zeitwerk::Loader]
|
140
|
+
def on_unload(loader)
|
141
|
+
autoloads.delete_if { |_path, object| object == loader }
|
142
|
+
inceptions.delete_if { |_cpath, (_path, object)| object == loader }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
@loaders = []
|
147
|
+
@loaders_managing_gems = {}
|
148
|
+
@autoloads = {}
|
149
|
+
@inceptions = {}
|
150
|
+
end
|
151
|
+
end
|
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zeitwerk
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.alpha
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Xavier Noria
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-01-18 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |2
|
14
|
+
Zeitwerk implements constant autoloading with Ruby semantics. Each gem
|
15
|
+
and application may have their own independent autoloader, with its own
|
16
|
+
configuration, inflector, and logger. Supports autoloading, preloading,
|
17
|
+
reloading, and eager loading.
|
18
|
+
email: fxn@hashref.com
|
19
|
+
executables: []
|
20
|
+
extensions: []
|
21
|
+
extra_rdoc_files: []
|
22
|
+
files:
|
23
|
+
- README.md
|
24
|
+
- lib/zeitwerk.rb
|
25
|
+
- lib/zeitwerk/gem_inflector.rb
|
26
|
+
- lib/zeitwerk/inflector.rb
|
27
|
+
- lib/zeitwerk/kernel.rb
|
28
|
+
- lib/zeitwerk/loader.rb
|
29
|
+
- lib/zeitwerk/registry.rb
|
30
|
+
- lib/zeitwerk/version.rb
|
31
|
+
homepage: https://github.com/fxn/zeitwerk
|
32
|
+
licenses:
|
33
|
+
- MIT
|
34
|
+
metadata: {}
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 2.4.4
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 1.3.1
|
49
|
+
requirements: []
|
50
|
+
rubygems_version: 3.0.1
|
51
|
+
signing_key:
|
52
|
+
specification_version: 4
|
53
|
+
summary: Efficient and thread-safe constant autoloader
|
54
|
+
test_files: []
|