im 0.1.6 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 -185
- metadata +25 -61
- data/CHANGELOG.md +0 -31
- data/Gemfile +0 -8
- data/Gemfile.lock +0 -37
- 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
|