zeitwerk 1.0.0.alpha → 1.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8109591e515376b3aa028f266d2d354be134aa38cc7b062f7130afc1640f91d
4
- data.tar.gz: b9c0118d2cbd8aa5dba6bf22be756f3c7ad448c7320958f76e53fc898ad55b76
3
+ metadata.gz: cb5151594ce18ccd7919a1ed6b7860107ce1bfc1a02a4f9b038d77cc91ce2450
4
+ data.tar.gz: e403ef92cc0a14b09bcb54d15d53ba821ad3d72e7d6216c3b0e10e6e9c0734f9
5
5
  SHA512:
6
- metadata.gz: 51bc3e2567f0ef5c6199e56799ab0d851ba7006e35ccf3813d4dde949fe44d9af6848ac4da219e359a017275d86b0fae7fc4657660c8a0c0737ffb02ac78f614
7
- data.tar.gz: 4d7051b8d27fc70fd45aaff66c78e00881e758b1d7db1c551026233874f837351aaa72336c1b8fa083b6a83fbc6987f16f0687e9fcf92df8a41f36812d5fad2d
6
+ metadata.gz: 10f8af59a66e1462c622cfb6f5316c98808113830041f094861b1fa07a2e9a6734d1ea64a01ebc664e9b5dcf20844b443e98c6bbf196fec99a0dcfee76010331
7
+ data.tar.gz: 226489835ceb91d696138a966b34054755d9fb1e6bb3245cc028d03e156f21df6a949ee329f5575be1501e8bc143afe532037ff95af2a3631e82fe72963e9479
data/README.md CHANGED
@@ -1,22 +1,45 @@
1
- # WIP — NOT PUBLISHED — API AND DOCS IN FLUX
2
-
3
1
  # Zeitwerk
4
2
 
5
3
  [![Build Status](https://travis-ci.com/fxn/zeitwerk.svg?branch=master)](https://travis-ci.com/fxn/zeitwerk)
6
4
 
5
+ <!-- TOC -->
6
+
7
+ - [Zeitwerk](#zeitwerk)
8
+ - [Introduction](#introduction)
9
+ - [Synopsis](#synopsis)
10
+ - [File structure](#file-structure)
11
+ - [Implicit namespaces](#implicit-namespaces)
12
+ - [Explicit namespaces](#explicit-namespaces)
13
+ - [Nested root directories](#nested-root-directories)
14
+ - [Usage](#usage)
15
+ - [Setup](#setup)
16
+ - [Reloading](#reloading)
17
+ - [Eager loading](#eager-loading)
18
+ - [Preloading](#preloading)
19
+ - [Inflection](#inflection)
20
+ - [Zeitwerk::Inflector](#zeitwerkinflector)
21
+ - [Zeitwerk::GemInflector](#zeitwerkgeminflector)
22
+ - [Custom inflector](#custom-inflector)
23
+ - [Logging](#logging)
24
+ - [Ignoring parts of the project](#ignoring-parts-of-the-project)
25
+ - [Supported Ruby versions](#supported-ruby-versions)
26
+ - [Motivation](#motivation)
27
+ - [Thanks](#thanks)
28
+ - [License](#license)
29
+
30
+ <!-- /TOC -->
31
+
7
32
  ## Introduction
8
33
 
9
34
  Zeitwerk is an efficient and thread-safe code loader for Ruby.
10
35
 
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.
36
+ Given a conventional file structure, Zeitwerk loads your project's 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. This feature is efficient, thread-safe, and matches Ruby's semantics for constants.
14
37
 
15
38
  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
39
 
17
40
  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
41
 
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.
42
+ Finally, in some production setups it may be optimal to eager load all code upfront. Zeitwerk is able to do that too.
20
43
 
21
44
  ## Synopsis
22
45
 
@@ -26,7 +49,7 @@ Main interface for gems:
26
49
  # lib/my_gem.rb (main file)
27
50
 
28
51
  require "zeitwerk"
29
- Zeitwerk::Loader.for_gem.setup
52
+ Zeitwerk::Loader.for_gem.setup # ready!
30
53
 
31
54
  module MyGem
32
55
  # ...
@@ -38,10 +61,10 @@ Main generic interface:
38
61
  ```ruby
39
62
  loader = Zeitwerk::Loader.new
40
63
  loader.push_dir(...)
41
- loader.setup
64
+ loader.setup # ready!
42
65
  ```
43
66
 
44
- Zeitwerk is ready to right after the `setup` call.
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 and remain active.
45
68
 
46
69
  Later, you can reload if you want to:
47
70
 
@@ -49,21 +72,21 @@ Later, you can reload if you want to:
49
72
  loader.reload
50
73
  ```
51
74
 
52
- and you can also eager load:
75
+ and you can also eager load all the code:
53
76
 
54
77
  ```ruby
55
78
  loader.eager_load
56
79
  ```
57
80
 
58
- Broadcast `eager_load` to all loaders:
81
+ It is also possible to broadcast `eager_load` to all instances:
59
82
 
60
83
  ```
61
84
  Zeitwerk::Loader.eager_load_all
62
85
  ```
63
86
 
64
- ## Compatible file structure
87
+ ## File structure
65
88
 
66
- To have a file structure compatible with Zeitwerk, just name files and directories after the name of the classes and modules they define:
89
+ To have a file structure Zeitwerk can work with, just name files and directories after the name of the classes and modules they define:
67
90
 
68
91
  ```
69
92
  lib/my_gem.rb -> MyGem
@@ -79,7 +102,7 @@ loader.push_dir(Rails.root.join("app/models"))
79
102
  loader.push_dir(Rails.root.join("app/controllers"))
80
103
  ```
81
104
 
82
- you get the mappings
105
+ Zeitwerk understands that their respective files and subdirectories belong to the root namespace:
83
106
 
84
107
  ```
85
108
  app/models/user.rb -> User
@@ -98,85 +121,144 @@ app/controllers/admin/users_controller.rb -> Admin::UsersController
98
121
 
99
122
  ### Explicit namespaces
100
123
 
101
- Classes and modules that act as namespaces can also be explictly defined, though. For instance, consider
124
+ Classes and modules that act as namespaces can also be explicitly defined, though. For instance, consider
102
125
 
103
126
  ```
104
- app/models/hotel.rb -> Hotel
105
- app/models/hotel/pricing -> Hotel::Pricing
127
+ app/models/hotel.rb -> Hotel
128
+ app/models/hotel/pricing.rb -> Hotel::Pricing
106
129
  ```
107
130
 
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`.
131
+ Zeitwerk does not autovivify a `Hotel` module in that case. The file `app/models/hotel.rb` explicitly defines `Hotel` and Zeitwerk loads it as needed before going for `Hotel::Pricing`.
109
132
 
110
- ## Synopsis
133
+ ### Nested root directories
111
134
 
135
+ Root directories should not be ideally nested, but Zeitwerk supports them because in Rails, for example, both `app/models` and `app/models/concerns` belong to the autoload paths.
112
136
 
137
+ Zeitwerk detects nested root directories, and treats them as roots only. In the example above, `concerns` is not considered to be a namespace below `app/models`. For example, the file:
113
138
 
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.
139
+ ```
140
+ app/models/concerns/geolocatable.rb
141
+ ```
115
142
 
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.
143
+ should define `Geolocatable`, not `Concerns::Geolocatable`.
117
144
 
118
- ## Use case: Gems
145
+ ## Usage
119
146
 
120
- To use Zeitwerk in a gem just throw these couple of lines in the main file:
147
+ ### Setup
148
+
149
+ Loaders are ready to load code right after calling `setup` on them:
121
150
 
122
151
  ```ruby
123
- # lib/my_gem.rb
152
+ loader.setup
153
+ ```
124
154
 
125
- require "zeitwerk"
126
- Zeitwerk::Loader.for_gem.setup
155
+ Customization should generally be done before that call. In particular, in the generic interface you may set the root directories from which you want to load files:
127
156
 
128
- module MyGem
129
- # ...
130
- end
157
+ ```ruby
158
+ loader.push_dir(...)
159
+ loader.push_dir(...)
160
+ loader.setup
131
161
  ```
132
162
 
133
- The loader of your gem is exclusive, it has its own configuration, inflector, and logger.
163
+ 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.
134
164
 
135
- ## Use case: Generic interface
165
+ ### Reloading
136
166
 
137
- The generic interface to use Zeitwerk in a different context is
167
+ In order to reload code:
138
168
 
139
169
  ```ruby
140
- require "zeitwerk"
170
+ loader.reload
171
+ ```
141
172
 
142
- loader = Zeitwerk::Loader.new
143
- loader.push_dir("...")
173
+ 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.
174
+
175
+ It is important to highlight that this is and instance method. Therefore, reloading the code of a project managed by a particular loader does _not_ reload the code of other gems using Zeitwerk at all.
176
+
177
+ 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
+
179
+ ### Eager loading
180
+
181
+ Zeitwerk instances are able to eager load their managed files:
182
+
183
+ ```ruby
184
+ loader.eager_load
185
+ ```
186
+
187
+ You can opt-out of eager loading individual files or directories:
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)
144
193
  loader.setup
194
+ loader.eager_load # won't go into the directories with db/cache adapters
145
195
  ```
146
196
 
147
- A loader instantiated that way is independent of other loaders. It has its own configuration, inflector, and logger.
197
+ Files and directories excluded from eager loading can still be loaded on demand, so an idiom like this is possible:
148
198
 
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.
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.
204
+
205
+ If you want to eager load yourself and all dependencies using Zeitwerk, you can broadcast the `eager_load` call to all instances:
206
+
207
+ ```ruby
208
+ Zeitwerk::Loader.eager_load_all
209
+ ```
210
+
211
+ In that case, exclusions are per autoloader, and so will apply to each of them accordingly.
212
+
213
+ This may be handy in top-level services, like web applications.
214
+
215
+ ### Preloading
216
+
217
+ Zeitwerk instances are able to preload files and directories.
218
+
219
+ ```ruby
220
+ loader.preload("app/models/videogame.rb")
221
+ loader.preload("app/models/book.rb")
222
+ ```
223
+
224
+ The example above depicts several calls are supported, but `preload` accepts multiple arguments and arrays of strings as well.
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.
150
229
 
151
- ## Inflection
230
+ 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
+
232
+ ### Inflection
152
233
 
153
234
  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
235
 
155
- ### Zeitwerk::Inflector
236
+ #### Zeitwerk::Inflector
156
237
 
157
- This is a super basic inflector that converts snake case to camel case preserving upper case letters:
238
+ This is a very basic inflector that converts snake case to camel case:
158
239
 
159
240
  ```
160
241
  user -> User
161
242
  users_controller -> UsersController
162
243
  html_parser -> HtmlParser
163
- HTML_parser -> HTMLParser
164
244
  ```
165
245
 
246
+ There are no inflection rules or global configuration that can affect this inflector. It is deterministic.
247
+
166
248
  This is the default inflector.
167
249
 
168
- ### Zeitwerk::GemInflector
250
+ #### Zeitwerk::GemInflector
169
251
 
170
252
  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
253
 
172
- ### Custom inflector
254
+ #### Custom inflector
173
255
 
174
256
  The inflectors that ship with Zeitwerk are deterministic and simple. But you can configure your own:
175
257
 
176
258
  ```ruby
177
259
  # frozen_string_literal: true
178
260
 
179
- class MyInflector < Zeitwerk::Inflector
261
+ class MyInflector < Zeitwerk::Inflector # or Zeitwerk::GemInflector
180
262
  def camelize(basename, _abspath)
181
263
  case basename
182
264
  when "api"
@@ -197,113 +279,44 @@ The second argument, `abspath`, is a string with the absolute path to the file o
197
279
  Then, assign the inflector before calling `setup`:
198
280
 
199
281
  ```
200
- loader = Zeitwerk::Loader.new
201
282
  loader.inflector = MyInflector.new
202
- loader.setup
203
283
  ```
204
284
 
205
- ## Logging
285
+ This needs to be assigned before the call to `setup`.
206
286
 
207
- Zeitwerk is silent by default, but you can configure a callable as logger.
287
+ ### Logging
208
288
 
209
- In the case of gems, for example:
289
+ Zeitwerk is silent by default, but you can configure a callable as logger:
210
290
 
211
291
  ```ruby
212
- require "zeitwerk"
213
- loader = Zeitwerk::Loader.for_gem
214
292
  loader.logger = method(:puts)
215
- loader.setup
216
293
  ```
217
294
 
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.
295
+ If there is a logger configured, the loader is going to print traces when autoloads are set, files loaded, and modules autovivified.
219
296
 
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.
297
+ 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.
221
298
 
222
- ## Reloading
299
+ ### Ignoring parts of the project
223
300
 
224
- In order to reload code, unload and setup:
301
+ Sometimes it might be convenient to tell Zeitwerk to completely ignore some particular file or directory. For example, let's suppose that your gem decorates something in `Kernel`:
225
302
 
226
303
  ```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.
304
+ # lib/my_gem/core_ext/kernel.rb
232
305
 
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]}")
306
+ Kernel.module_eval do
307
+ # ...
308
+ end
259
309
  ```
260
310
 
261
- You can even opt-out from eager loading entirely:
311
+ That file does not follow the conventions and you need to tell Zeitwerk:
262
312
 
263
313
  ```ruby
264
- require "zeitwerk"
265
- loader = Zeitwerk::Loader.for_gem
266
- loader.do_not_eager_load(__dir__)
314
+ kernel_ext = File.expand_path("my_gem/core_ext/kernel.rb", __dir__)
315
+ loader.ignore(kernel_ext)
267
316
  loader.setup
268
317
  ```
269
318
 
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
- ```
319
+ You can pass several arguments to this method, also an array of strings. And you can call `ignore` multiple times too.
307
320
 
308
321
  ## Supported Ruby versions
309
322
 
@@ -2,17 +2,18 @@
2
2
 
3
3
  module Zeitwerk
4
4
  class GemInflector < Inflector
5
- # @param gem_entry_point [String]
5
+ # @param root_file [String]
6
6
  def initialize(root_file)
7
- namespace = File.basename(root_file, ".rb")
8
- @version_file = File.join(namespace, "version.rb")
7
+ namespace = File.basename(root_file, ".rb")
8
+ lib_dir = File.dirname(root_file)
9
+ @version_file = File.join(lib_dir, namespace, "version.rb")
9
10
  end
10
11
 
11
12
  # @param basename [String]
12
13
  # @param abspath [String]
13
14
  # @return [String]
14
15
  def camelize(basename, abspath)
15
- basename == "version" && abspath.end_with?(@version_file) ? "VERSION" : super
16
+ (basename == "version" && abspath == @version_file) ? "VERSION" : super
16
17
  end
17
18
  end
18
19
  end
@@ -2,19 +2,17 @@
2
2
 
3
3
  module Zeitwerk
4
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.
5
+ # Very basic snake case -> camel case conversion.
7
6
  #
8
7
  # Zeitwerk::Inflector.camelize("post", ...) # => "Post"
9
8
  # Zeitwerk::Inflector.camelize("users_controller", ...) # => "UsersController"
10
9
  # Zeitwerk::Inflector.camelize("api", ...) # => "Api"
11
- # Zeitwerk::Inflector.camelize("HTML", ...) # => "HTML"
12
10
  #
13
11
  # @param basename [String]
14
12
  # @param _abspath [String]
15
13
  # @return [String]
16
14
  def camelize(basename, _abspath)
17
- basename.gsub(/(?:\A|_)(\w)/) { $1.capitalize }
15
+ basename.split('_').map(&:capitalize).join
18
16
  end
19
17
  end
20
18
  end
@@ -4,20 +4,10 @@ require "set"
4
4
 
5
5
  module Zeitwerk
6
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
7
  # @return [#camelize]
16
8
  attr_accessor :inflector
17
9
 
18
- # An optional callable to log when a constant is loaded, `nil` by default.
19
- #
20
- # @return [nil, #call]
10
+ # @return [#call, nil]
21
11
  attr_accessor :logger
22
12
 
23
13
  # Absolute paths of directories from which you want to load constants. This
@@ -26,11 +16,9 @@ module Zeitwerk
26
16
  # Stored in a hash to preserve order, easily handle duplicates, and also be
27
17
  # able to have a fast lookup, needed for detecting nested paths.
28
18
  #
29
- # {
30
- # "/Users/fxn/blog/app/assets" => true,
31
- # "/Users/fxn/blog/app/channels" => true,
32
- # ...
33
- # }
19
+ # "/Users/fxn/blog/app/assets" => true,
20
+ # "/Users/fxn/blog/app/channels" => true,
21
+ # ...
34
22
  #
35
23
  # @private
36
24
  # @return [{String => true}]
@@ -51,8 +39,9 @@ module Zeitwerk
51
39
  # Maps real absolute paths for which an autoload has been set to their
52
40
  # corresponding parent class or module and constant name.
53
41
  #
54
- # "/Users/fxn/blog/app/models/user.rb" => [Object, "User"]
42
+ # "/Users/fxn/blog/app/models/user.rb" => [Object, "User"],
55
43
  # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, "Pricing"]
44
+ # ...
56
45
  #
57
46
  # @private
58
47
  # @return [{String => (Module, String)}]
@@ -84,6 +73,10 @@ module Zeitwerk
84
73
  # @return [Mutex]
85
74
  attr_reader :mutex
86
75
 
76
+ # This tracer listens to `:class` events, and it is used to support explicit
77
+ # namespaces. Benchmarks have shown the tracer does not impact performance
78
+ # in any measurable way.
79
+ #
87
80
  # @private
88
81
  # @return [TracePoint]
89
82
  attr_reader :tracer
@@ -92,17 +85,16 @@ module Zeitwerk
92
85
  self.inflector = Inflector.new
93
86
 
94
87
  @dirs = {}
95
- @autoloads = {}
96
88
  @preloads = []
97
- @lazy_subdirs = {}
98
89
  @ignored = Set.new
90
+ @autoloads = {}
91
+ @lazy_subdirs = {}
99
92
  @eager_load_exclusions = Set.new
100
- @mutex = Mutex.new
101
- @setup = false
102
- @eager_loaded = false
103
93
 
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.
94
+ @mutex = Mutex.new
95
+ @setup = false
96
+ @eager_loaded = false
97
+
106
98
  @tracer = TracePoint.trace(:class) do |tp|
107
99
  unless lazy_subdirs.empty? # do not even compute the hash key if not needed
108
100
  if subdirs = lazy_subdirs.delete(tp.self.name)
@@ -148,7 +140,7 @@ module Zeitwerk
148
140
  end
149
141
  end
150
142
 
151
- # Files or directories to be totally ignored by Zeitwerk.
143
+ # Files or directories to be totally ignored.
152
144
  #
153
145
  # @param paths [<String, Pathname, <String, Pathname>>]
154
146
  # @return [void]
@@ -156,11 +148,7 @@ module Zeitwerk
156
148
  mutex.synchronize { ignored.merge(expand_paths(paths)) }
157
149
  end
158
150
 
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.
151
+ # Sets autoloads in the root namespace and preloads files, if any.
164
152
  #
165
153
  # @return [void]
166
154
  def setup
@@ -181,6 +169,7 @@ module Zeitwerk
181
169
  # else, they are eligible for garbage collection, which would effectively
182
170
  # unload them.
183
171
  #
172
+ # @private
184
173
  # @return [void]
185
174
  def unload
186
175
  mutex.synchronize do
@@ -204,7 +193,11 @@ module Zeitwerk
204
193
  end
205
194
  end
206
195
 
207
- # Docs pending.
196
+ # Unloads all loaded code, and calls setup again so that the loader is able
197
+ # to pick any changes in the file system.
198
+ #
199
+ # This method is not thread-safe, please see how this can be achieved by
200
+ # client code in the README of the project.
208
201
  #
209
202
  # @return [void]
210
203
  def reload
@@ -212,8 +205,10 @@ module Zeitwerk
212
205
  setup
213
206
  end
214
207
 
215
- # Eager loads in the root directories, recursively. Files do not need to be
216
- # in `$LOAD_PATH`, absolute file names are used.
208
+ # Eager loads all files in the root directories, recursively. Files do not
209
+ # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
210
+ # are not eager loaded. You can opt-out specifically in specific files and
211
+ # directories with `do_not_eager_load`.
217
212
  #
218
213
  # @return [void]
219
214
  def eager_load
@@ -228,7 +223,8 @@ module Zeitwerk
228
223
  end
229
224
  end
230
225
 
231
- # Let eager load ignore the given files or directories.
226
+ # Let eager load ignore the given files or directories. The constants
227
+ # defined in those files are still autoloadable.
232
228
  #
233
229
  # @param paths [<String, Pathname, <String, Pathname>>]
234
230
  # @return [void]
@@ -236,7 +232,7 @@ module Zeitwerk
236
232
  mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
237
233
  end
238
234
 
239
- # --- Class methods -----------------------------------------------------------------------------
235
+ # --- Class methods ---------------------------------------------------------------------------
240
236
 
241
237
  # This is a shortcut for
242
238
  #
@@ -257,14 +253,14 @@ module Zeitwerk
257
253
  Registry.loader_for_gem(called_from)
258
254
  end
259
255
 
260
- # Broadcasts `eager_load` to all instances.
256
+ # Broadcasts `eager_load` to all loaders.
261
257
  #
262
258
  # @return [void]
263
259
  def self.eager_load_all
264
260
  Registry.loaders.each(&:eager_load)
265
261
  end
266
262
 
267
- # --- Calbacks ----------------------------------------------------------------------------------
263
+ # --- Callbacks -------------------------------------------------------------------------------
268
264
 
269
265
  # Callback invoked from Kernel when a managed file is loaded.
270
266
  #
@@ -293,9 +289,8 @@ module Zeitwerk
293
289
  end
294
290
  end
295
291
 
296
- private # ---------------------------------------------------------------------------------------
292
+ private # -------------------------------------------------------------------------------------
297
293
 
298
- # @private
299
294
  # @return [<String>]
300
295
  def actual_dirs
301
296
  dirs.each_key.reject { |dir| ignored.member?(dir) }
@@ -303,6 +298,7 @@ module Zeitwerk
303
298
 
304
299
  # @param dir [String]
305
300
  # @param parent [Module]
301
+ # @return [void]
306
302
  def set_autoloads_in_dir(dir, parent)
307
303
  each_abspath(dir) do |abspath|
308
304
  cname = inflector.camelize(File.basename(abspath, ".rb"), abspath)
@@ -320,16 +316,16 @@ module Zeitwerk
320
316
  end
321
317
  end
322
318
 
323
- # @param subdir [String]
324
319
  # @param parent [Module]
325
320
  # @paran cname [String]
321
+ # @param subdir [String]
326
322
  # @return [void]
327
323
  def autoload_subdir(parent, cname, subdir)
328
324
  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.
325
+ # If there is already an autoload for this cname, maybe there are
326
+ # multiple directories defining the namespace, or the cname is going to
327
+ # be defined in a file (explicit namespace). In either case, we do not
328
+ # need to issue another autoload, the existing one is fine.
333
329
  (lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
334
330
  elsif !parent.const_defined?(cname, false)
335
331
  # First time we find this namespace, set an autoload for it.
@@ -342,9 +338,9 @@ module Zeitwerk
342
338
  end
343
339
  end
344
340
 
345
- # @param file [String]
346
341
  # @param parent [Module]
347
342
  # @paran cname [String]
343
+ # @param file [String]
348
344
  # @return [void]
349
345
  def autoload_file(parent, cname, file)
350
346
  if autoload_path = autoload_for?(parent, cname)
@@ -390,12 +386,13 @@ module Zeitwerk
390
386
 
391
387
  # @param parent [Module]
392
388
  # @param cname [String]
393
- # @return [nil, String]
389
+ # @return [String, nil]
394
390
  def autoload_for?(parent, cname)
395
391
  parent.autoload?(cname) || Registry.inception?(cpath(parent, cname))
396
392
  end
397
393
 
398
394
  # @param dir [String]
395
+ # @return [void]
399
396
  def eager_load_dir(dir)
400
397
  each_abspath(dir) do |abspath|
401
398
  next if eager_load_exclusions.member?(abspath)
@@ -445,7 +442,7 @@ module Zeitwerk
445
442
  # @param cname [String]
446
443
  # @return [String]
447
444
  def cpath(parent, cname)
448
- parent.equal?(Object) ? cname : "#{parent}::#{cname}"
445
+ parent.equal?(Object) ? cname : "#{parent.name}::#{cname}"
449
446
  end
450
447
 
451
448
  # @param dir [String]
@@ -19,17 +19,14 @@ module Zeitwerk
19
19
 
20
20
  # Maps real paths to the loaders responsible for them.
21
21
  #
22
- # This information is used by our decorated Kernel#require to be able to
23
- # invoke callbacks.
22
+ # This information is used by our decorated `Kernel#require` to be able to
23
+ # invoke callbacks and autovivify modules.
24
24
  #
25
25
  # @private
26
26
  # @return [{String => Zeitwerk::Loader}]
27
27
  attr_reader :autoloads
28
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 (!!!!)
29
+ # This hash table addresses an edge case in which an autoload is ignored.
33
30
  #
34
31
  # For example, let's suppose we want to autoload in a gem like this:
35
32
  #
@@ -41,16 +38,19 @@ module Zeitwerk
41
38
  # module MyGem
42
39
  # end
43
40
  #
44
- # if you require "my_gem", this happens while setting up autoloads:
41
+ # if you require "my_gem", as Bundler would do, this happens while setting
42
+ # up autoloads:
45
43
  #
46
- # 1. Object.autoload?(:MyGem) returns `nil`.
44
+ # 1. Object.autoload?(:MyGem) returns `nil` because the autoload for
45
+ # the constant is issued by Zeitwerk while the same file is being
46
+ # required.
47
47
  # 2. The constant `MyGem` is undefined while setup runs.
48
48
  #
49
49
  # Therefore, a directory `lib/my_gem` would autovivify a module according to
50
50
  # the existing information. But that would be wrong.
51
51
  #
52
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
53
+ # paths that are in this situation ---in the example above, "MyGem"--- and
54
54
  # take this collection into account for the autovivification logic.
55
55
  #
56
56
  # Note that you cannot generally address this by moving the setup code
@@ -61,16 +61,6 @@ module Zeitwerk
61
61
  # include MyConcern
62
62
  # end
63
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
64
  # @private
75
65
  # @return [{String => (String, Zeitwerk::Loader)}]
76
66
  attr_reader :inceptions
@@ -79,12 +69,13 @@ module Zeitwerk
79
69
  #
80
70
  # @private
81
71
  # @param loader [Zeitwerk::Loader]
72
+ # @return [void]
82
73
  def register_loader(loader)
83
74
  loaders << loader
84
75
  end
85
76
 
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.
77
+ # This method returns always a loader, the same instance for the same root
78
+ # file. That is how Zeitwerk::Loader.for_gem is idempotent.
88
79
  #
89
80
  # @private
90
81
  # @param root_file [String]
@@ -101,12 +92,14 @@ module Zeitwerk
101
92
  # @private
102
93
  # @param loader [Zeitwerk::Loader]
103
94
  # @param realpath [String]
95
+ # @return [void]
104
96
  def register_autoload(loader, realpath)
105
97
  autoloads[realpath] = loader
106
98
  end
107
99
 
108
100
  # @private
109
101
  # @param realpath [String]
102
+ # @return [void]
110
103
  def unregister_autoload(realpath)
111
104
  autoloads.delete(realpath)
112
105
  end
@@ -115,6 +108,7 @@ module Zeitwerk
115
108
  # @param cpath [String]
116
109
  # @param realpath [String]
117
110
  # @param loader [Zeitwerk::Loader]
111
+ # @return [void]
118
112
  def register_inception(cpath, realpath, loader)
119
113
  inceptions[cpath] = [realpath, loader]
120
114
  end
@@ -137,6 +131,7 @@ module Zeitwerk
137
131
 
138
132
  # @private
139
133
  # @param loader [Zeitwerk::Loader]
134
+ # @return [void]
140
135
  def on_unload(loader)
141
136
  autoloads.delete_if { |_path, object| object == loader }
142
137
  inceptions.delete_if { |_cpath, (_path, object)| object == loader }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- VERSION = "1.0.0.alpha"
4
+ VERSION = "1.0.0.beta"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeitwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.alpha
4
+ version: 1.0.0.beta
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xavier Noria