im 0.1.5 → 0.2.0
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.
- checksums.yaml +4 -4
- data/MIT-LICENSE +21 -0
- data/README.md +245 -32
- data/lib/im/const_path.rb +48 -0
- data/lib/im/error.rb +24 -0
- data/lib/im/explicit_namespace.rb +96 -0
- data/lib/im/gem_inflector.rb +17 -0
- data/lib/im/gem_loader.rb +65 -0
- data/lib/im/inflector.rb +46 -0
- data/lib/im/internal.rb +12 -0
- data/lib/im/kernel.rb +34 -9
- data/lib/im/loader/callbacks.rb +93 -0
- data/lib/im/loader/config.rb +346 -0
- data/lib/im/loader/eager_load.rb +214 -0
- data/lib/im/loader/helpers.rb +123 -0
- data/lib/im/loader.rb +586 -0
- data/lib/im/module_const_added.rb +63 -0
- data/lib/im/registry.rb +166 -0
- data/lib/im/version.rb +1 -1
- data/lib/im.rb +18 -172
- metadata +24 -60
- data/CHANGELOG.md +0 -28
- data/Gemfile +0 -10
- data/Gemfile.lock +0 -182
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -8
- data/lib/im/module.rb +0 -9
- data/lib/im/ruby_version_check.rb +0 -22
data/lib/im/kernel.rb
CHANGED
@@ -1,19 +1,44 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Kernel
|
4
|
-
|
4
|
+
module_function
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
6
|
+
alias_method :im_original_require, :require
|
7
|
+
class << self
|
8
|
+
alias_method :im_original_require, :require
|
10
9
|
end
|
11
10
|
|
12
|
-
|
11
|
+
# @sig (String) -> true | false
|
12
|
+
def require(path)
|
13
|
+
filetype, feature_path = $:.resolve_feature_path(path)
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
if (loader = Im::Registry.loader_for(path)) ||
|
16
|
+
((loader = Im::Registry.loader_for(feature_path)) && (path = feature_path))
|
17
|
+
if :rb == filetype
|
18
|
+
if loaded = !$LOADED_FEATURES.include?(feature_path)
|
19
|
+
$LOADED_FEATURES << feature_path
|
20
|
+
begin
|
21
|
+
load path, loader
|
22
|
+
rescue => e
|
23
|
+
$LOADED_FEATURES.delete(feature_path)
|
24
|
+
raise e
|
25
|
+
end
|
26
|
+
loader.on_file_autoloaded(path)
|
27
|
+
end
|
28
|
+
loaded
|
29
|
+
else
|
30
|
+
loader.on_dir_autoloaded(path)
|
31
|
+
true
|
32
|
+
end
|
33
|
+
else
|
34
|
+
required = im_original_require(path)
|
35
|
+
if required
|
36
|
+
abspath = $LOADED_FEATURES.last
|
37
|
+
if loader = Im::Registry.loader_for(abspath)
|
38
|
+
loader.on_file_autoloaded(abspath)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
required
|
17
42
|
end
|
18
43
|
end
|
19
44
|
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Im::Loader::Callbacks
|
4
|
+
# Invoked from our decorated Kernel#require when a managed file is autoloaded.
|
5
|
+
#
|
6
|
+
# @private
|
7
|
+
# @sig (String) -> void
|
8
|
+
def on_file_autoloaded(file)
|
9
|
+
cref = autoloads.delete(file)
|
10
|
+
|
11
|
+
relative_cpath = relative_cpath(*cref)
|
12
|
+
to_unload[relative_cpath] = [file, cref] if reloading_enabled?
|
13
|
+
Im::Registry.unregister_autoload(file)
|
14
|
+
|
15
|
+
if cdef?(*cref)
|
16
|
+
obj = cget(*cref)
|
17
|
+
if obj.is_a?(Module)
|
18
|
+
register_module_name(obj, relative_cpath)
|
19
|
+
Im::Registry.register_autoloaded_module(obj, relative_cpath, self)
|
20
|
+
end
|
21
|
+
log("constant #{relative_cpath} loaded from file #{file}") if logger
|
22
|
+
run_on_load_callbacks(relative_cpath, obj, file) unless on_load_callbacks.empty?
|
23
|
+
else
|
24
|
+
raise Im::NameError.new("expected file #{file} to define constant #{cpath(*cref)}, but didn't", cref.last)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Invoked from our decorated Kernel#require when a managed directory is
|
29
|
+
# autoloaded.
|
30
|
+
#
|
31
|
+
# @private
|
32
|
+
# @sig (String) -> void
|
33
|
+
def on_dir_autoloaded(dir)
|
34
|
+
# Module#autoload does not serialize concurrent requires, and we handle
|
35
|
+
# directories ourselves, so the callback needs to account for concurrency.
|
36
|
+
#
|
37
|
+
# Multi-threading would introduce a race condition here in which thread t1
|
38
|
+
# autovivifies the module, and while autoloads for its children are being
|
39
|
+
# set, thread t2 autoloads the same namespace.
|
40
|
+
#
|
41
|
+
# Without the mutex and subsequent delete call, t2 would reset the module.
|
42
|
+
# That not only would reassign the constant (undesirable per se) but, worse,
|
43
|
+
# the module object created by t2 wouldn't have any of the autoloads for its
|
44
|
+
# children, since t1 would have correctly deleted its namespace_dirs entry.
|
45
|
+
mutex2.synchronize do
|
46
|
+
if cref = autoloads.delete(dir)
|
47
|
+
autovivified_module = cref[0].const_set(cref[1], Module.new)
|
48
|
+
relative_cpath = relative_cpath(*cref)
|
49
|
+
register_module_name(autovivified_module, relative_cpath)
|
50
|
+
Im::Registry.register_autoloaded_module(autovivified_module, relative_cpath, self)
|
51
|
+
log("module #{relative_cpath} autovivified from directory #{dir}") if logger
|
52
|
+
|
53
|
+
to_unload[relative_cpath] = [dir, cref] if reloading_enabled?
|
54
|
+
|
55
|
+
# We don't unregister `dir` in the registry because concurrent threads
|
56
|
+
# wouldn't find a loader associated to it in Kernel#require and would
|
57
|
+
# try to require the directory. Instead, we are going to keep track of
|
58
|
+
# these to be able to unregister later if eager loading.
|
59
|
+
autoloaded_dirs << dir
|
60
|
+
|
61
|
+
on_namespace_loaded(relative_cpath)
|
62
|
+
|
63
|
+
run_on_load_callbacks(relative_cpath, autovivified_module, dir) unless on_load_callbacks.empty?
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Invoked when a class or module is created or reopened, either from the
|
69
|
+
# tracer or from module autovivification. If the namespace has matching
|
70
|
+
# subdirectories, we descend into them now.
|
71
|
+
#
|
72
|
+
# @private
|
73
|
+
# @sig (Module) -> void
|
74
|
+
def on_namespace_loaded(module_name)
|
75
|
+
if dirs = namespace_dirs.delete(module_name)
|
76
|
+
dirs.each do |dir|
|
77
|
+
set_autoloads_in_dir(dir, cget(self, module_name))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# @sig (String, Object) -> void
|
85
|
+
def run_on_load_callbacks(cpath, value, abspath)
|
86
|
+
# Order matters. If present, run the most specific one.
|
87
|
+
callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)
|
88
|
+
callbacks&.each { |c| c.call(value, abspath) }
|
89
|
+
|
90
|
+
callbacks = on_load_callbacks[:ANY]
|
91
|
+
callbacks&.each { |c| c.call(cpath, value, abspath) }
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,346 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
require "securerandom"
|
5
|
+
|
6
|
+
module Im::Loader::Config
|
7
|
+
extend Im::Internal
|
8
|
+
|
9
|
+
# @sig #camelize
|
10
|
+
attr_accessor :inflector
|
11
|
+
|
12
|
+
# @sig #call | #debug | nil
|
13
|
+
attr_accessor :logger
|
14
|
+
|
15
|
+
# Absolute paths of the root directories, as a set:
|
16
|
+
#
|
17
|
+
# #<Set: {
|
18
|
+
# "/Users/fxn/blog/app/channels",
|
19
|
+
# "/Users/fxn/blog/app/adapters",
|
20
|
+
# ...
|
21
|
+
# }>
|
22
|
+
#
|
23
|
+
# This is a private collection maintained by the loader. The public
|
24
|
+
# interface for it is `push_dir` and `dirs`.
|
25
|
+
#
|
26
|
+
# @sig Array[String]
|
27
|
+
attr_reader :root_dirs
|
28
|
+
internal :root_dirs
|
29
|
+
|
30
|
+
# Absolute paths of files, directories, or glob patterns to be totally
|
31
|
+
# ignored.
|
32
|
+
#
|
33
|
+
# @sig Set[String]
|
34
|
+
attr_reader :ignored_glob_patterns
|
35
|
+
private :ignored_glob_patterns
|
36
|
+
|
37
|
+
# The actual collection of absolute file and directory names at the time the
|
38
|
+
# ignored glob patterns were expanded. Computed on setup, and recomputed on
|
39
|
+
# reload.
|
40
|
+
#
|
41
|
+
# @sig Set[String]
|
42
|
+
attr_reader :ignored_paths
|
43
|
+
private :ignored_paths
|
44
|
+
|
45
|
+
# Absolute paths of directories or glob patterns to be collapsed.
|
46
|
+
#
|
47
|
+
# @sig Set[String]
|
48
|
+
attr_reader :collapse_glob_patterns
|
49
|
+
private :collapse_glob_patterns
|
50
|
+
|
51
|
+
# The actual collection of absolute directory names at the time the collapse
|
52
|
+
# glob patterns were expanded. Computed on setup, and recomputed on reload.
|
53
|
+
#
|
54
|
+
# @sig Set[String]
|
55
|
+
attr_reader :collapse_dirs
|
56
|
+
private :collapse_dirs
|
57
|
+
|
58
|
+
# Absolute paths of files or directories not to be eager loaded.
|
59
|
+
#
|
60
|
+
# @sig Set[String]
|
61
|
+
attr_reader :eager_load_exclusions
|
62
|
+
private :eager_load_exclusions
|
63
|
+
|
64
|
+
# User-oriented callbacks to be fired on setup and on reload.
|
65
|
+
#
|
66
|
+
# @sig Array[{ () -> void }]
|
67
|
+
attr_reader :on_setup_callbacks
|
68
|
+
private :on_setup_callbacks
|
69
|
+
|
70
|
+
# User-oriented callbacks to be fired when a constant is loaded.
|
71
|
+
#
|
72
|
+
# @sig Hash[String, Array[{ (Object, String) -> void }]]
|
73
|
+
# Hash[Symbol, Array[{ (String, Object, String) -> void }]]
|
74
|
+
attr_reader :on_load_callbacks
|
75
|
+
private :on_load_callbacks
|
76
|
+
|
77
|
+
# User-oriented callbacks to be fired before constants are removed.
|
78
|
+
#
|
79
|
+
# @sig Hash[String, Array[{ (Object, String) -> void }]]
|
80
|
+
# Hash[Symbol, Array[{ (String, Object, String) -> void }]]
|
81
|
+
attr_reader :on_unload_callbacks
|
82
|
+
private :on_unload_callbacks
|
83
|
+
|
84
|
+
def initialize
|
85
|
+
@inflector = Im::Inflector.new
|
86
|
+
@logger = self.class.default_logger
|
87
|
+
@tag = SecureRandom.hex(3)
|
88
|
+
@initialized_at = Time.now
|
89
|
+
@root_dirs = Set.new
|
90
|
+
@ignored_glob_patterns = Set.new
|
91
|
+
@ignored_paths = Set.new
|
92
|
+
@collapse_glob_patterns = Set.new
|
93
|
+
@collapse_dirs = Set.new
|
94
|
+
@eager_load_exclusions = Set.new
|
95
|
+
@reloading_enabled = false
|
96
|
+
@on_setup_callbacks = []
|
97
|
+
@on_load_callbacks = {}
|
98
|
+
@on_unload_callbacks = {}
|
99
|
+
end
|
100
|
+
|
101
|
+
# Pushes `path` to the list of root directories.
|
102
|
+
#
|
103
|
+
# Raises `Im::Error` if `path` does not exist, or if another loader in
|
104
|
+
# the same process already manages that directory or one of its ascendants or
|
105
|
+
# descendants.
|
106
|
+
#
|
107
|
+
# @raise [Im::Error]
|
108
|
+
# @sig (String | Pathname, Module) -> void
|
109
|
+
def push_dir(path)
|
110
|
+
abspath = File.expand_path(path)
|
111
|
+
if dir?(abspath)
|
112
|
+
raise_if_conflicting_directory(abspath)
|
113
|
+
root_dirs << abspath
|
114
|
+
else
|
115
|
+
raise Im::Error, "the root directory #{abspath} does not exist"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns the loader's tag.
|
120
|
+
#
|
121
|
+
# Implemented as a method instead of via attr_reader for symmetry with the
|
122
|
+
# writer below.
|
123
|
+
#
|
124
|
+
# @sig () -> String
|
125
|
+
def tag
|
126
|
+
@tag
|
127
|
+
end
|
128
|
+
|
129
|
+
# Sets a tag for the loader, useful for logging.
|
130
|
+
#
|
131
|
+
# @sig (#to_s) -> void
|
132
|
+
def tag=(tag)
|
133
|
+
@tag = tag.to_s
|
134
|
+
end
|
135
|
+
|
136
|
+
# Rturns an array with the absolute paths of the root directories as strings.
|
137
|
+
# If `ignored` is falsey (default), ignored root directories are filtered out.
|
138
|
+
#
|
139
|
+
# These are read-only collections, please add to them with `push_dir`.
|
140
|
+
#
|
141
|
+
# @sig () -> Array[String] | Hash[String, Module]
|
142
|
+
def dirs(ignored: false)
|
143
|
+
if ignored || ignored_paths.empty?
|
144
|
+
root_dirs
|
145
|
+
else
|
146
|
+
root_dirs.reject { |root_dir| ignored_path?(root_dir) }
|
147
|
+
end.freeze
|
148
|
+
end
|
149
|
+
|
150
|
+
# You need to call this method before setup in order to be able to reload.
|
151
|
+
# There is no way to undo this, either you want to reload or you don't.
|
152
|
+
#
|
153
|
+
# @raise [Im::Error]
|
154
|
+
# @sig () -> void
|
155
|
+
def enable_reloading
|
156
|
+
mutex.synchronize do
|
157
|
+
break if @reloading_enabled
|
158
|
+
|
159
|
+
if @setup
|
160
|
+
raise Im::Error, "cannot enable reloading after setup"
|
161
|
+
else
|
162
|
+
@reloading_enabled = true
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# @sig () -> bool
|
168
|
+
def reloading_enabled?
|
169
|
+
@reloading_enabled
|
170
|
+
end
|
171
|
+
|
172
|
+
# Let eager load ignore the given files or directories. The constants defined
|
173
|
+
# in those files are still autoloadable.
|
174
|
+
#
|
175
|
+
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
176
|
+
def do_not_eager_load(*paths)
|
177
|
+
mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
|
178
|
+
end
|
179
|
+
|
180
|
+
# Configure files, directories, or glob patterns to be totally ignored.
|
181
|
+
#
|
182
|
+
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
183
|
+
def ignore(*glob_patterns)
|
184
|
+
glob_patterns = expand_paths(glob_patterns)
|
185
|
+
mutex.synchronize do
|
186
|
+
ignored_glob_patterns.merge(glob_patterns)
|
187
|
+
ignored_paths.merge(expand_glob_patterns(glob_patterns))
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Configure directories or glob patterns to be collapsed.
|
192
|
+
#
|
193
|
+
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
194
|
+
def collapse(*glob_patterns)
|
195
|
+
glob_patterns = expand_paths(glob_patterns)
|
196
|
+
mutex.synchronize do
|
197
|
+
collapse_glob_patterns.merge(glob_patterns)
|
198
|
+
collapse_dirs.merge(expand_glob_patterns(glob_patterns))
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Configure a block to be called after setup and on each reload.
|
203
|
+
# If setup was already done, the block runs immediately.
|
204
|
+
#
|
205
|
+
# @sig () { () -> void } -> void
|
206
|
+
def on_setup(&block)
|
207
|
+
mutex.synchronize do
|
208
|
+
on_setup_callbacks << block
|
209
|
+
block.call if @setup
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Configure a block to be invoked once a certain constant path is loaded.
|
214
|
+
# Supports multiple callbacks, and if there are many, they are executed in
|
215
|
+
# the order in which they were defined.
|
216
|
+
#
|
217
|
+
# loader.on_load("SomeApiClient") do |klass, _abspath|
|
218
|
+
# klass.endpoint = "https://api.dev"
|
219
|
+
# end
|
220
|
+
#
|
221
|
+
# Can also be configured for any constant loaded:
|
222
|
+
#
|
223
|
+
# loader.on_load do |cpath, value, abspath|
|
224
|
+
# # ...
|
225
|
+
# end
|
226
|
+
#
|
227
|
+
# @raise [TypeError]
|
228
|
+
# @sig (String) { (Object, String) -> void } -> void
|
229
|
+
# (:ANY) { (String, Object, String) -> void } -> void
|
230
|
+
def on_load(cpath = :ANY, &block)
|
231
|
+
raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
|
232
|
+
|
233
|
+
mutex.synchronize { _on_load(cpath, &block) }
|
234
|
+
end
|
235
|
+
|
236
|
+
# @sig (String) { (Object, String) -> void } -> void
|
237
|
+
# (:ANY) { (String, Object, String) -> void } -> void
|
238
|
+
private def _on_load(cpath, &block)
|
239
|
+
(on_load_callbacks[cpath] ||= []) << block
|
240
|
+
end
|
241
|
+
|
242
|
+
# Configure a block to be invoked right before a certain constant is removed.
|
243
|
+
# Supports multiple callbacks, and if there are many, they are executed in the
|
244
|
+
# order in which they were defined.
|
245
|
+
#
|
246
|
+
# loader.on_unload("Country") do |klass, _abspath|
|
247
|
+
# klass.clear_cache
|
248
|
+
# end
|
249
|
+
#
|
250
|
+
# Can also be configured for any removed constant:
|
251
|
+
#
|
252
|
+
# loader.on_unload do |cpath, value, abspath|
|
253
|
+
# # ...
|
254
|
+
# end
|
255
|
+
#
|
256
|
+
# @raise [TypeError]
|
257
|
+
# @sig (String) { (Object) -> void } -> void
|
258
|
+
# (:ANY) { (String, Object) -> void } -> void
|
259
|
+
def on_unload(cpath = :ANY, &block)
|
260
|
+
raise TypeError, "on_unload only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
|
261
|
+
|
262
|
+
mutex.synchronize do
|
263
|
+
(on_unload_callbacks[cpath] ||= []) << block
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# Logs to `$stdout`, handy shortcut for debugging.
|
268
|
+
#
|
269
|
+
# @sig () -> void
|
270
|
+
def log!
|
271
|
+
@logger = ->(msg) { puts msg }
|
272
|
+
end
|
273
|
+
|
274
|
+
# Returns true if the argument has been configured to be ignored, or is a
|
275
|
+
# descendant of an ignored directory.
|
276
|
+
#
|
277
|
+
# @sig (String) -> bool
|
278
|
+
internal def ignores?(abspath)
|
279
|
+
# Common use case.
|
280
|
+
return false if ignored_paths.empty?
|
281
|
+
|
282
|
+
walk_up(abspath) do |abspath|
|
283
|
+
return true if ignored_path?(abspath)
|
284
|
+
return false if root_dir?(abspath)
|
285
|
+
end
|
286
|
+
|
287
|
+
false
|
288
|
+
end
|
289
|
+
|
290
|
+
# @sig (String) -> bool
|
291
|
+
private def ignored_path?(abspath)
|
292
|
+
ignored_paths.member?(abspath)
|
293
|
+
end
|
294
|
+
|
295
|
+
# @sig () -> Array[String]
|
296
|
+
private def actual_roots
|
297
|
+
root_dirs.reject do |root_dir|
|
298
|
+
!dir?(root_dir) || ignored_path?(root_dir)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
# @sig (String) -> bool
|
303
|
+
private def root_dir?(dir)
|
304
|
+
root_dirs.include?(dir)
|
305
|
+
end
|
306
|
+
|
307
|
+
# @sig (String) -> bool
|
308
|
+
private def excluded_from_eager_load?(abspath)
|
309
|
+
# Optimize this common use case.
|
310
|
+
return false if eager_load_exclusions.empty?
|
311
|
+
|
312
|
+
walk_up(abspath) do |abspath|
|
313
|
+
return true if eager_load_exclusions.member?(abspath)
|
314
|
+
return false if root_dir?(abspath)
|
315
|
+
end
|
316
|
+
|
317
|
+
false
|
318
|
+
end
|
319
|
+
|
320
|
+
# @sig (String) -> bool
|
321
|
+
private def collapse?(dir)
|
322
|
+
collapse_dirs.member?(dir)
|
323
|
+
end
|
324
|
+
|
325
|
+
# @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
|
326
|
+
private def expand_paths(paths)
|
327
|
+
paths.flatten.map! { |path| File.expand_path(path) }
|
328
|
+
end
|
329
|
+
|
330
|
+
# @sig (Array[String]) -> Array[String]
|
331
|
+
private def expand_glob_patterns(glob_patterns)
|
332
|
+
# Note that Dir.glob works with regular file names just fine. That is,
|
333
|
+
# glob patterns technically need no wildcards.
|
334
|
+
glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
|
335
|
+
end
|
336
|
+
|
337
|
+
# @sig () -> void
|
338
|
+
private def recompute_ignored_paths
|
339
|
+
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
340
|
+
end
|
341
|
+
|
342
|
+
# @sig () -> void
|
343
|
+
private def recompute_collapse_dirs
|
344
|
+
collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
|
345
|
+
end
|
346
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
module Im::Loader::EagerLoad
|
2
|
+
# Eager loads all files in the root directories, recursively. Files do not
|
3
|
+
# need to be in `$LOAD_PATH`, absolute file names are used. Ignored and
|
4
|
+
# shadowed files are not eager loaded. You can opt-out specifically in
|
5
|
+
# specific files and directories with `do_not_eager_load`, and that can be
|
6
|
+
# overridden passing `force: true`.
|
7
|
+
#
|
8
|
+
# @sig (true | false) -> void
|
9
|
+
def eager_load(force: false)
|
10
|
+
mutex.synchronize do
|
11
|
+
break if @eager_loaded
|
12
|
+
raise Im::SetupRequired unless @setup
|
13
|
+
|
14
|
+
log("eager load start") if logger
|
15
|
+
|
16
|
+
actual_roots.each do |root_dir|
|
17
|
+
actual_eager_load_dir(root_dir, self, force: force)
|
18
|
+
end
|
19
|
+
|
20
|
+
autoloaded_dirs.each do |autoloaded_dir|
|
21
|
+
Im::Registry.unregister_autoload(autoloaded_dir)
|
22
|
+
end
|
23
|
+
autoloaded_dirs.clear
|
24
|
+
|
25
|
+
@eager_loaded = true
|
26
|
+
|
27
|
+
log("eager load end") if logger
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# @sig (String | Pathname) -> void
|
32
|
+
def eager_load_dir(path)
|
33
|
+
raise Im::SetupRequired unless @setup
|
34
|
+
|
35
|
+
abspath = File.expand_path(path)
|
36
|
+
|
37
|
+
raise Im::Error.new("#{abspath} is not a directory") unless dir?(abspath)
|
38
|
+
|
39
|
+
cnames = []
|
40
|
+
|
41
|
+
found_root = false
|
42
|
+
walk_up(abspath) do |dir|
|
43
|
+
return if ignored_path?(dir)
|
44
|
+
return if eager_load_exclusions.member?(dir)
|
45
|
+
|
46
|
+
break if found_root = root_dirs.include?(dir)
|
47
|
+
|
48
|
+
unless collapse?(dir)
|
49
|
+
basename = File.basename(dir)
|
50
|
+
cnames << inflector.camelize(basename, dir).to_sym
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
raise Im::Error.new("I do not manage #{abspath}") unless found_root
|
55
|
+
|
56
|
+
return if @eager_loaded
|
57
|
+
|
58
|
+
namespace = self
|
59
|
+
cnames.reverse_each do |cname|
|
60
|
+
# Can happen if there are no Ruby files. This is not an error condition,
|
61
|
+
# the directory is actually managed. Could have Ruby files later.
|
62
|
+
return unless cdef?(namespace, cname)
|
63
|
+
namespace = cget(namespace, cname)
|
64
|
+
end
|
65
|
+
|
66
|
+
# A shortcircuiting test depends on the invocation of this method. Please
|
67
|
+
# keep them in sync if refactored.
|
68
|
+
actual_eager_load_dir(abspath, namespace)
|
69
|
+
end
|
70
|
+
|
71
|
+
# @sig (Module) -> void
|
72
|
+
def eager_load_namespace(mod)
|
73
|
+
raise Im::SetupRequired unless @setup
|
74
|
+
|
75
|
+
unless mod.is_a?(Module)
|
76
|
+
raise Im::Error, "#{mod.inspect} is not a class or module object"
|
77
|
+
end
|
78
|
+
|
79
|
+
return if @eager_loaded
|
80
|
+
|
81
|
+
mod_name = Im.cpath(mod)
|
82
|
+
|
83
|
+
actual_roots.each do |root_dir|
|
84
|
+
if mod.equal?(self)
|
85
|
+
# A shortcircuiting test depends on the invocation of this method.
|
86
|
+
# Please keep them in sync if refactored.
|
87
|
+
actual_eager_load_dir(root_dir, self)
|
88
|
+
else
|
89
|
+
eager_load_child_namespace(mod, mod_name, root_dir)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Loads the given Ruby file.
|
95
|
+
#
|
96
|
+
# Raises if the argument is ignored, shadowed, or not managed by the receiver.
|
97
|
+
#
|
98
|
+
# The method is implemented as `constantize` for files, in a sense, to be able
|
99
|
+
# to descend orderly and make sure the file is loadable.
|
100
|
+
#
|
101
|
+
# @sig (String | Pathname) -> void
|
102
|
+
def load_file(path)
|
103
|
+
abspath = File.expand_path(path)
|
104
|
+
|
105
|
+
raise Im::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
|
106
|
+
raise Im::Error.new("#{abspath} is not a Ruby file") if dir?(abspath) || !ruby?(abspath)
|
107
|
+
raise Im::Error.new("#{abspath} is ignored") if ignored_path?(abspath)
|
108
|
+
|
109
|
+
basename = File.basename(abspath, ".rb")
|
110
|
+
base_cname = inflector.camelize(basename, abspath).to_sym
|
111
|
+
|
112
|
+
root_included = false
|
113
|
+
cnames = []
|
114
|
+
|
115
|
+
walk_up(File.dirname(abspath)) do |dir|
|
116
|
+
raise Im::Error.new("#{abspath} is ignored") if ignored_path?(dir)
|
117
|
+
|
118
|
+
break if root_included = root_dirs.include?(dir)
|
119
|
+
|
120
|
+
unless collapse?(dir)
|
121
|
+
basename = File.basename(dir)
|
122
|
+
cnames << inflector.camelize(basename, dir).to_sym
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
raise Im::Error.new("I do not manage #{abspath}") unless root_included
|
127
|
+
|
128
|
+
namespace = self
|
129
|
+
cnames.reverse_each do |cname|
|
130
|
+
namespace = cget(namespace, cname)
|
131
|
+
end
|
132
|
+
|
133
|
+
raise Im::Error.new("#{abspath} is shadowed") if shadowed_file?(abspath)
|
134
|
+
|
135
|
+
cget(namespace, base_cname)
|
136
|
+
end
|
137
|
+
|
138
|
+
# The caller is responsible for making sure `namespace` is the namespace that
|
139
|
+
# corresponds to `dir`.
|
140
|
+
#
|
141
|
+
# @sig (String, Module, Boolean) -> void
|
142
|
+
private def actual_eager_load_dir(dir, namespace, force: false)
|
143
|
+
honour_exclusions = !force
|
144
|
+
return if honour_exclusions && excluded_from_eager_load?(dir)
|
145
|
+
|
146
|
+
log("eager load directory #{dir} start") if logger
|
147
|
+
|
148
|
+
queue = [[dir, namespace]]
|
149
|
+
while to_eager_load = queue.shift
|
150
|
+
dir, namespace = to_eager_load
|
151
|
+
|
152
|
+
ls(dir) do |basename, abspath|
|
153
|
+
next if honour_exclusions && eager_load_exclusions.member?(abspath)
|
154
|
+
|
155
|
+
if ruby?(abspath)
|
156
|
+
if (cref = autoloads[abspath]) && !shadowed_file?(abspath)
|
157
|
+
cget(*cref)
|
158
|
+
end
|
159
|
+
else
|
160
|
+
if collapse?(abspath)
|
161
|
+
queue << [abspath, namespace]
|
162
|
+
else
|
163
|
+
cname = inflector.camelize(basename, abspath).to_sym
|
164
|
+
queue << [abspath, cget(namespace, cname)]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
log("eager load directory #{dir} end") if logger
|
171
|
+
end
|
172
|
+
|
173
|
+
# In order to invoke this method, the caller has to ensure `child` is a
|
174
|
+
# strict namespace descendendant of `root_namespace`.
|
175
|
+
#
|
176
|
+
# @sig (Module, String, Module, Boolean) -> void
|
177
|
+
private def eager_load_child_namespace(child, child_name, root_dir)
|
178
|
+
suffix = child_name
|
179
|
+
suffix = suffix.delete_prefix("#{self}::")
|
180
|
+
|
181
|
+
# These directories are at the same namespace level, there may be more if
|
182
|
+
# we find collapsed ones. As we scan, we look for matches for the first
|
183
|
+
# segment, and store them in `next_dirs`. If there are any, we look for
|
184
|
+
# the next segments in those matches. Repeat.
|
185
|
+
#
|
186
|
+
# If we exhaust the search locating directories that match all segments,
|
187
|
+
# we just need to eager load those ones.
|
188
|
+
dirs = [root_dir]
|
189
|
+
next_dirs = []
|
190
|
+
|
191
|
+
suffix.split("::").each do |segment|
|
192
|
+
while dir = dirs.shift
|
193
|
+
ls(dir) do |basename, abspath|
|
194
|
+
next unless dir?(abspath)
|
195
|
+
|
196
|
+
if collapse?(abspath)
|
197
|
+
dirs << abspath
|
198
|
+
elsif segment == inflector.camelize(basename, abspath)
|
199
|
+
next_dirs << abspath
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
return if next_dirs.empty?
|
205
|
+
|
206
|
+
dirs.replace(next_dirs)
|
207
|
+
next_dirs.clear
|
208
|
+
end
|
209
|
+
|
210
|
+
dirs.each do |dir|
|
211
|
+
actual_eager_load_dir(dir, child)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|