zeitwerk 1.0.0.beta → 1.0.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +78 -34
- data/lib/zeitwerk/inflector.rb +1 -1
- data/lib/zeitwerk/loader.rb +47 -45
- data/lib/zeitwerk/registry.rb +1 -1
- data/lib/zeitwerk/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cfc51c080dd7eb9b63e869a04b6eeeb14c0098b64de32d0ef872bba22a6d85d2
|
4
|
+
data.tar.gz: 0d1f743c91aefa0dd073f8f9d05c97991849cd5aa2552633048c15e93269015c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 05fba3abfe897598b45650dd837bb8e7fe4ae19f8ddd43bac1e934e2da18d1374bec14c13c46e43ad37f5cc31f991ddd9c9e429455aa7014bc4e6760b690a03d
|
7
|
+
data.tar.gz: 9ae4f99de1c0e2ace6be146c80fb7384cc893f57f2ec4e6970b44b85ce80d9cf2aa95300b2aac18cb87bc56dfd07c3d155b6b7898c21216b303a1ebc24575414
|
data/README.md
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# Zeitwerk
|
2
2
|
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/zeitwerk.svg)](https://badge.fury.io/rb/zeitwerk)
|
3
4
|
[![Build Status](https://travis-ci.com/fxn/zeitwerk.svg?branch=master)](https://travis-ci.com/fxn/zeitwerk)
|
4
5
|
|
5
6
|
<!-- TOC -->
|
@@ -22,6 +23,8 @@
|
|
22
23
|
- [Custom inflector](#custom-inflector)
|
23
24
|
- [Logging](#logging)
|
24
25
|
- [Ignoring parts of the project](#ignoring-parts-of-the-project)
|
26
|
+
- [Edge cases](#edge-cases)
|
27
|
+
- [Pronunciation](#pronunciation)
|
25
28
|
- [Supported Ruby versions](#supported-ruby-versions)
|
26
29
|
- [Motivation](#motivation)
|
27
30
|
- [Thanks](#thanks)
|
@@ -64,7 +67,7 @@ loader.push_dir(...)
|
|
64
67
|
loader.setup # ready!
|
65
68
|
```
|
66
69
|
|
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
|
70
|
+
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.
|
68
71
|
|
69
72
|
Later, you can reload if you want to:
|
70
73
|
|
@@ -80,7 +83,7 @@ loader.eager_load
|
|
80
83
|
|
81
84
|
It is also possible to broadcast `eager_load` to all instances:
|
82
85
|
|
83
|
-
```
|
86
|
+
```ruby
|
84
87
|
Zeitwerk::Loader.eager_load_all
|
85
88
|
```
|
86
89
|
|
@@ -128,7 +131,16 @@ app/models/hotel.rb -> Hotel
|
|
128
131
|
app/models/hotel/pricing.rb -> Hotel::Pricing
|
129
132
|
```
|
130
133
|
|
131
|
-
|
134
|
+
There, `app/models/hotel.rb` defines `Hotel`, and thus Zeitwerk does not autovivify a module.
|
135
|
+
|
136
|
+
The classes and modules from the namespace are already available in the body of the class or module defining it:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
class Hotel < ApplicationRecord
|
140
|
+
include Pricing # works
|
141
|
+
...
|
142
|
+
end
|
143
|
+
```
|
132
144
|
|
133
145
|
### Nested root directories
|
134
146
|
|
@@ -162,6 +174,8 @@ loader.setup
|
|
162
174
|
|
163
175
|
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.
|
164
176
|
|
177
|
+
Zeitwerk works internally only with absolute paths to avoid costly file searches in `$LOAD_PATH`. Indeed, the root directories do not even need to belong to `$LOAD_PATH`, everything just works by design if they don't.
|
178
|
+
|
165
179
|
### Reloading
|
166
180
|
|
167
181
|
In order to reload code:
|
@@ -170,12 +184,16 @@ In order to reload code:
|
|
170
184
|
loader.reload
|
171
185
|
```
|
172
186
|
|
173
|
-
Generally speaking, reloading is useful
|
187
|
+
Generally speaking, reloading is useful while developing running services like web applications. Gems that implement regular libraries, so to speak, won't normally have a use case for reloading.
|
188
|
+
|
189
|
+
Reloading removes the currently loaded classes and modules, resets the loader so that it will pick whatever is in the file system now, and runs preloads if there are any.
|
174
190
|
|
175
|
-
It is important to highlight that this is
|
191
|
+
It is important to highlight that this is an instance method. Don't worry about the project dependencies, their loaders are independent.
|
176
192
|
|
177
193
|
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
194
|
|
195
|
+
On reloading, client code has to update anything that would otherwise be storing a stale object. For example, if the routing layer of a web framework stores controller class objects or instances in internal structures, on reload it has to refresh them somehow, possibly reevaluating routes.
|
196
|
+
|
179
197
|
### Eager loading
|
180
198
|
|
181
199
|
Zeitwerk instances are able to eager load their managed files:
|
@@ -184,23 +202,7 @@ Zeitwerk instances are able to eager load their managed files:
|
|
184
202
|
loader.eager_load
|
185
203
|
```
|
186
204
|
|
187
|
-
|
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)
|
193
|
-
loader.setup
|
194
|
-
loader.eager_load # won't go into the directories with db/cache adapters
|
195
|
-
```
|
196
|
-
|
197
|
-
Files and directories excluded from eager loading can still be loaded on demand, so an idiom like this is possible:
|
198
|
-
|
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.
|
205
|
+
That skips ignored files and directories.
|
204
206
|
|
205
207
|
If you want to eager load yourself and all dependencies using Zeitwerk, you can broadcast the `eager_load` call to all instances:
|
206
208
|
|
@@ -208,8 +210,6 @@ If you want to eager load yourself and all dependencies using Zeitwerk, you can
|
|
208
210
|
Zeitwerk::Loader.eager_load_all
|
209
211
|
```
|
210
212
|
|
211
|
-
In that case, exclusions are per autoloader, and so will apply to each of them accordingly.
|
212
|
-
|
213
213
|
This may be handy in top-level services, like web applications.
|
214
214
|
|
215
215
|
### Preloading
|
@@ -221,14 +221,12 @@ loader.preload("app/models/videogame.rb")
|
|
221
221
|
loader.preload("app/models/book.rb")
|
222
222
|
```
|
223
223
|
|
224
|
-
The
|
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.
|
224
|
+
The call can happen before `setup` (preloads during setup), or after `setup` (preloads on the spot). Each reload preloads too.
|
229
225
|
|
230
226
|
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
227
|
|
228
|
+
The example above depicts several calls are supported, but `preload` accepts multiple arguments and arrays of strings as well.
|
229
|
+
|
232
230
|
### Inflection
|
233
231
|
|
234
232
|
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.
|
@@ -258,7 +256,7 @@ The inflectors that ship with Zeitwerk are deterministic and simple. But you can
|
|
258
256
|
```ruby
|
259
257
|
# frozen_string_literal: true
|
260
258
|
|
261
|
-
class MyInflector < Zeitwerk::Inflector
|
259
|
+
class MyInflector < Zeitwerk::Inflector
|
262
260
|
def camelize(basename, _abspath)
|
263
261
|
case basename
|
264
262
|
when "api"
|
@@ -276,13 +274,13 @@ The first argument, `basename`, is a string with the basename of the file or dir
|
|
276
274
|
|
277
275
|
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.
|
278
276
|
|
279
|
-
Then, assign the inflector
|
277
|
+
Then, assign the inflector:
|
280
278
|
|
281
|
-
```
|
279
|
+
```ruby
|
282
280
|
loader.inflector = MyInflector.new
|
283
281
|
```
|
284
282
|
|
285
|
-
This needs to be
|
283
|
+
This needs to be done before calling `setup`.
|
286
284
|
|
287
285
|
### Logging
|
288
286
|
|
@@ -292,7 +290,7 @@ Zeitwerk is silent by default, but you can configure a callable as logger:
|
|
292
290
|
loader.logger = method(:puts)
|
293
291
|
```
|
294
292
|
|
295
|
-
If there is a logger configured, the loader is going to print traces when autoloads are set, files loaded, and modules autovivified.
|
293
|
+
If there is a logger configured, the loader is going to print traces when autoloads are set, files loaded, and modules autovivified. While reloading, removed autoloads and unloaded objects are also traced.
|
296
294
|
|
297
295
|
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.
|
298
296
|
|
@@ -318,6 +316,52 @@ loader.setup
|
|
318
316
|
|
319
317
|
You can pass several arguments to this method, also an array of strings. And you can call `ignore` multiple times too.
|
320
318
|
|
319
|
+
Another use case for ignoring files is the adapter pattern. Let's imagine your project talks to databases, supports several, and has adapters for each one of them. Those adapters may have top-level `require` calls that load their respective drivers, but you don't want your users to install them all, only the one they are going to use. On the other hand, if your code is eager loaded by you or a parent project (with `Zeitwerk::Loader.eager_load_all`), those `require` calls are going to be executed. Ignoring the adapters prevents that:
|
320
|
+
|
321
|
+
```ruby
|
322
|
+
db_adapters = File.expand_path("my_gem/db_adapters", __dir__)
|
323
|
+
loader.ignore(db_adapters)
|
324
|
+
loader.setup
|
325
|
+
```
|
326
|
+
|
327
|
+
The chosen adapter, then, has to be loaded by hand somehow:
|
328
|
+
|
329
|
+
```ruby
|
330
|
+
require "my_gem/db_adapters/#{config[:db_adapter]}"
|
331
|
+
```
|
332
|
+
|
333
|
+
### Edge cases
|
334
|
+
|
335
|
+
A class or module that acts as a namespace:
|
336
|
+
|
337
|
+
```ruby
|
338
|
+
# trip.rb
|
339
|
+
class Trip
|
340
|
+
include Geolocation
|
341
|
+
end
|
342
|
+
|
343
|
+
# trip/geolocation.rb
|
344
|
+
module Trip::Geolocation
|
345
|
+
...
|
346
|
+
end
|
347
|
+
```
|
348
|
+
|
349
|
+
has to be defined with the `class` or `module` keywords, as in the example above.
|
350
|
+
|
351
|
+
For technical reasons, raw constant assignment is not supported:
|
352
|
+
|
353
|
+
```ruby
|
354
|
+
# trip.rb
|
355
|
+
Trip = Class.new { ... } # NOT SUPPORTED
|
356
|
+
Trip = Struct.new { ... } # NOT SUPPORTED
|
357
|
+
```
|
358
|
+
|
359
|
+
This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
|
360
|
+
|
361
|
+
## Pronunciation
|
362
|
+
|
363
|
+
"Zeitwerk" is pronounced [this way](https://raw.githubusercontent.com/fxn/zeitwerk/master/extras/zeitwerk_pronunciation.mp3).
|
364
|
+
|
321
365
|
## Supported Ruby versions
|
322
366
|
|
323
367
|
Zeitwerk works with MRI 2.4.4 and above.
|
data/lib/zeitwerk/inflector.rb
CHANGED
data/lib/zeitwerk/loader.rb
CHANGED
@@ -33,7 +33,7 @@ module Zeitwerk
|
|
33
33
|
# Absolute paths of files or directories to be totally ignored.
|
34
34
|
#
|
35
35
|
# @private
|
36
|
-
# @return [String]
|
36
|
+
# @return [Set<String>]
|
37
37
|
attr_reader :ignored
|
38
38
|
|
39
39
|
# Maps real absolute paths for which an autoload has been set to their
|
@@ -65,10 +65,6 @@ module Zeitwerk
|
|
65
65
|
# @return [{String => <String>}]
|
66
66
|
attr_reader :lazy_subdirs
|
67
67
|
|
68
|
-
# @private
|
69
|
-
# @return [Set]
|
70
|
-
attr_reader :eager_load_exclusions
|
71
|
-
|
72
68
|
# @private
|
73
69
|
# @return [Mutex]
|
74
70
|
attr_reader :mutex
|
@@ -84,18 +80,17 @@ module Zeitwerk
|
|
84
80
|
def initialize
|
85
81
|
self.inflector = Inflector.new
|
86
82
|
|
87
|
-
@dirs
|
88
|
-
@preloads
|
89
|
-
@ignored
|
90
|
-
@autoloads
|
91
|
-
@lazy_subdirs
|
92
|
-
@eager_load_exclusions = Set.new
|
83
|
+
@dirs = {}
|
84
|
+
@preloads = []
|
85
|
+
@ignored = Set.new
|
86
|
+
@autoloads = {}
|
87
|
+
@lazy_subdirs = {}
|
93
88
|
|
94
89
|
@mutex = Mutex.new
|
95
90
|
@setup = false
|
96
91
|
@eager_loaded = false
|
97
92
|
|
98
|
-
@tracer = TracePoint.
|
93
|
+
@tracer = TracePoint.new(:class) do |tp|
|
99
94
|
unless lazy_subdirs.empty? # do not even compute the hash key if not needed
|
100
95
|
if subdirs = lazy_subdirs.delete(tp.self.name)
|
101
96
|
subdirs.each { |subdir| set_autoloads_in_dir(subdir, tp.self) }
|
@@ -155,7 +150,6 @@ module Zeitwerk
|
|
155
150
|
mutex.synchronize do
|
156
151
|
unless @setup
|
157
152
|
actual_dirs.each { |dir| set_autoloads_in_dir(dir, Object) }
|
158
|
-
tracer.enable
|
159
153
|
do_preload
|
160
154
|
@setup = true
|
161
155
|
end
|
@@ -174,9 +168,13 @@ module Zeitwerk
|
|
174
168
|
def unload
|
175
169
|
mutex.synchronize do
|
176
170
|
autoloads.each do |path, (parent, cname)|
|
177
|
-
|
178
|
-
|
179
|
-
|
171
|
+
if parent.autoload?(cname)
|
172
|
+
parent.send(:remove_const, cname)
|
173
|
+
log("autoload for #{cpath(parent, cname)} removed") if logger
|
174
|
+
elsif parent.const_defined?(cname, false)
|
175
|
+
parent.send(:remove_const, cname)
|
176
|
+
log("#{cpath(parent, cname)} unloaded") if logger
|
177
|
+
end
|
180
178
|
|
181
179
|
# Let Kernel#require load the same path later again by removing it
|
182
180
|
# from $LOADED_FEATURES. We check the extension to avoid unnecessary
|
@@ -188,7 +186,7 @@ module Zeitwerk
|
|
188
186
|
|
189
187
|
Registry.on_unload(self)
|
190
188
|
|
191
|
-
|
189
|
+
disable_tracer
|
192
190
|
@setup = false
|
193
191
|
end
|
194
192
|
end
|
@@ -207,31 +205,19 @@ module Zeitwerk
|
|
207
205
|
|
208
206
|
# Eager loads all files in the root directories, recursively. Files do not
|
209
207
|
# need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
|
210
|
-
# are not eager loaded.
|
211
|
-
# directories with `do_not_eager_load`.
|
208
|
+
# are not eager loaded.
|
212
209
|
#
|
213
210
|
# @return [void]
|
214
211
|
def eager_load
|
215
212
|
mutex.synchronize do
|
216
213
|
unless @eager_loaded
|
217
|
-
actual_dirs.each
|
218
|
-
|
219
|
-
end
|
220
|
-
tracer.disable if eager_load_exclusions.empty?
|
214
|
+
actual_dirs.each { |dir| eager_load_dir(dir) }
|
215
|
+
disable_tracer
|
221
216
|
@eager_loaded = true
|
222
217
|
end
|
223
218
|
end
|
224
219
|
end
|
225
220
|
|
226
|
-
# Let eager load ignore the given files or directories. The constants
|
227
|
-
# defined in those files are still autoloadable.
|
228
|
-
#
|
229
|
-
# @param paths [<String, Pathname, <String, Pathname>>]
|
230
|
-
# @return [void]
|
231
|
-
def do_not_eager_load(*paths)
|
232
|
-
mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
|
233
|
-
end
|
234
|
-
|
235
221
|
# --- Class methods ---------------------------------------------------------------------------
|
236
222
|
|
237
223
|
# This is a shortcut for
|
@@ -270,7 +256,7 @@ module Zeitwerk
|
|
270
256
|
def on_file_loaded(file)
|
271
257
|
if logger
|
272
258
|
parent, cname = autoloads[file]
|
273
|
-
|
259
|
+
log("constant #{cpath(parent, cname)} loaded from file #{file}")
|
274
260
|
end
|
275
261
|
end
|
276
262
|
|
@@ -282,7 +268,7 @@ module Zeitwerk
|
|
282
268
|
def on_dir_loaded(dir)
|
283
269
|
parent, cname = autoloads[dir]
|
284
270
|
autovivified = parent.const_set(cname, Module.new)
|
285
|
-
|
271
|
+
log("module #{cpath(parent, cname)} autovivified from directory #{dir}") if logger
|
286
272
|
|
287
273
|
if subdirs = lazy_subdirs[cpath(parent, cname)]
|
288
274
|
subdirs.each { |subdir| set_autoloads_in_dir(subdir, autovivified) }
|
@@ -293,7 +279,7 @@ module Zeitwerk
|
|
293
279
|
|
294
280
|
# @return [<String>]
|
295
281
|
def actual_dirs
|
296
|
-
dirs.
|
282
|
+
dirs.keys.delete_if { |dir| ignored.member?(dir) }
|
297
283
|
end
|
298
284
|
|
299
285
|
# @param dir [String]
|
@@ -321,11 +307,11 @@ module Zeitwerk
|
|
321
307
|
# @param subdir [String]
|
322
308
|
# @return [void]
|
323
309
|
def autoload_subdir(parent, cname, subdir)
|
324
|
-
if autoload_for?(parent, cname)
|
325
|
-
|
326
|
-
#
|
327
|
-
#
|
328
|
-
#
|
310
|
+
if autoload = autoload_for?(parent, cname)
|
311
|
+
enable_tracer if ruby?(autoload)
|
312
|
+
# We do not need to issue another autoload, the existing one is enough
|
313
|
+
# no matter if it is for a file or a directory. Just remember the
|
314
|
+
# subdirectory has to be visited if the namespace is used.
|
329
315
|
(lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
|
330
316
|
elsif !parent.const_defined?(cname, false)
|
331
317
|
# First time we find this namespace, set an autoload for it.
|
@@ -351,7 +337,9 @@ module Zeitwerk
|
|
351
337
|
# class/module defined in this file.
|
352
338
|
autoloads.delete(autoload_path)
|
353
339
|
Registry.unregister_autoload(autoload_path)
|
340
|
+
|
354
341
|
set_autoload(parent, cname, file)
|
342
|
+
enable_tracer
|
355
343
|
elsif !parent.const_defined?(cname, false)
|
356
344
|
set_autoload(parent, cname, file)
|
357
345
|
end
|
@@ -369,9 +357,9 @@ module Zeitwerk
|
|
369
357
|
parent.autoload(cname, realpath)
|
370
358
|
if logger
|
371
359
|
if ruby?(realpath)
|
372
|
-
|
360
|
+
log("autoload set for #{cpath(parent, cname)}, to be loaded from #{realpath}")
|
373
361
|
else
|
374
|
-
|
362
|
+
log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{realpath}")
|
375
363
|
end
|
376
364
|
end
|
377
365
|
|
@@ -395,8 +383,6 @@ module Zeitwerk
|
|
395
383
|
# @return [void]
|
396
384
|
def eager_load_dir(dir)
|
397
385
|
each_abspath(dir) do |abspath|
|
398
|
-
next if eager_load_exclusions.member?(abspath)
|
399
|
-
|
400
386
|
if ruby?(abspath)
|
401
387
|
require abspath
|
402
388
|
elsif dir?(abspath)
|
@@ -434,7 +420,7 @@ module Zeitwerk
|
|
434
420
|
# @param file [String]
|
435
421
|
# @return [Boolean]
|
436
422
|
def do_preload_file(file)
|
437
|
-
|
423
|
+
log("preloading #{file}") if logger
|
438
424
|
require file
|
439
425
|
end
|
440
426
|
|
@@ -473,5 +459,21 @@ module Zeitwerk
|
|
473
459
|
def expand_paths(paths)
|
474
460
|
Array(paths).flatten.map { |path| File.expand_path(path) }
|
475
461
|
end
|
462
|
+
|
463
|
+
# @param message [String]
|
464
|
+
# @return [void]
|
465
|
+
def log(message)
|
466
|
+
logger.call("Zeitwerk: #{message}")
|
467
|
+
end
|
468
|
+
|
469
|
+
def enable_tracer
|
470
|
+
# We check enabled? because, looking at the C source code, enabling an
|
471
|
+
# enabled tracer does not seem to be a simple no-op.
|
472
|
+
tracer.enable if !tracer.enabled?
|
473
|
+
end
|
474
|
+
|
475
|
+
def disable_tracer
|
476
|
+
tracer.disable if tracer.enabled?
|
477
|
+
end
|
476
478
|
end
|
477
479
|
end
|
data/lib/zeitwerk/registry.rb
CHANGED
@@ -83,7 +83,7 @@ module Zeitwerk
|
|
83
83
|
def loader_for_gem(root_file)
|
84
84
|
loaders_managing_gems[root_file] ||= begin
|
85
85
|
Loader.new.tap do |loader|
|
86
|
-
loader.inflector =
|
86
|
+
loader.inflector = GemInflector.new(root_file)
|
87
87
|
loader.push_dir(File.dirname(root_file))
|
88
88
|
end
|
89
89
|
end
|
data/lib/zeitwerk/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zeitwerk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.
|
4
|
+
version: 1.0.0.beta2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Xavier Noria
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-01-
|
11
|
+
date: 2019-01-22 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |2
|
14
14
|
Zeitwerk implements constant autoloading with Ruby semantics. Each gem
|