zeitwerk 1.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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.
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zeitwerk
4
+ require_relative "zeitwerk/loader"
5
+ require_relative "zeitwerk/registry"
6
+ require_relative "zeitwerk/inflector"
7
+ require_relative "zeitwerk/gem_inflector"
8
+ require_relative "zeitwerk/kernel"
9
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zeitwerk
4
+ VERSION = "1.0.0.alpha"
5
+ 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: []