zeitwerk 1.0.0.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []