zeitwerk 1.0.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.com/fxn/zeitwerk.svg?branch=master)](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: []
|