opal-zeitwerk 0.0.1 → 0.0.2

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: 52b397fd96aa14fd2b196f67db7325010a66d7fb3fd94623d3ed4b957be1181c
4
- data.tar.gz: 4e1e32b7c582f1280f7c81c1c7ccbb69091521c0b6f0e1d73d832d1f845b689d
3
+ metadata.gz: 12375dd273e0ad8de57a3743951866692f94b85f8c9b8a3d0913d85fb19e6448
4
+ data.tar.gz: b4c8ad57b55115bd275a1006649be4f685db42a3bff85a41ed8a3eabcf88db13
5
5
  SHA512:
6
- metadata.gz: 436625b6e442aa157c064e6a51501b743d7b419900bb085771411f8c174bdf7ed35c2b79da1eb67f6a50c17f232178b5045545e8d1b70ead2d759ff406588733
7
- data.tar.gz: 047c69f853ecac1960a85f2dfd4f608f188cd47cb52ce6a6571ab63c55e6d49a92c6775e22240cf5a6a991726057f94b43608d4b65a5f68723bc946deca0b89e
6
+ metadata.gz: '012259b3e3b242baa7e372439257c07db34abe59c4b45f1bdd927a9a6cfe91e9084d383f8f16fb356eebff90a7d193b1b2e56ff353b325f71ff87dd24c7da144'
7
+ data.tar.gz: 9073faf1c136072053b80d0d531494fd95a885cb4013aff5926dad8f630bac705ce21875e270c15d6131575dbf2d1745dfc064791a7e441e31c0b80eef284d5d
@@ -0,0 +1,4 @@
1
+ require 'opal'
2
+ require 'opal/zeitwerk/version'
3
+
4
+ Opal.append_path File.expand_path(File.join(File.dirname(__FILE__), '..', 'opal')).untaint
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ module Opal
3
+ module Zeitwerk
4
+ VERSION = "0.0.2"
5
+ end
6
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ require 'opal'
3
+ require 'corelib/trace_point'
4
+ require 'securerandom'
5
+
6
+ module Zeitwerk
7
+ require_relative "zeitwerk/real_mod_name"
8
+ require_relative "zeitwerk/loader"
9
+ require_relative "zeitwerk/registry"
10
+ require_relative "zeitwerk/explicit_namespace"
11
+ require_relative "zeitwerk/inflector"
12
+ require_relative "zeitwerk/kernel"
13
+ require_relative "zeitwerk/error"
14
+ end
@@ -0,0 +1,10 @@
1
+ module Zeitwerk
2
+ class Error < StandardError
3
+ end
4
+
5
+ class ReloadingDisabledError < Error
6
+ end
7
+
8
+ class NameError < ::NameError
9
+ end
10
+ end
@@ -0,0 +1,71 @@
1
+ module Zeitwerk
2
+ # Centralizes the logic for the trace point used to detect the creation of
3
+ # explicit namespaces, needed to descend into matching subdirectories right
4
+ # after the constant has been defined.
5
+ #
6
+ # The implementation assumes an explicit namespace is managed by one loader.
7
+ # Loaders that reopen namespaces owned by other projects are responsible for
8
+ # loading their constant before setup. This is documented.
9
+ module ExplicitNamespace # :nodoc: all
10
+ class << self
11
+ include RealModName
12
+
13
+ # Maps constant paths that correspond to explicit namespaces according to
14
+ # the file system, to the loader responsible for them.
15
+ #
16
+ # @private
17
+ # @return [{String => Zeitwerk::Loader}]
18
+ attr_reader :cpaths
19
+
20
+ # @private
21
+ # @return [TracePoint]
22
+ attr_reader :tracer
23
+
24
+ # Asserts `cpath` corresponds to an explicit namespace for which `loader`
25
+ # is responsible.
26
+ #
27
+ # @private
28
+ # @param cpath [String]
29
+ # @param loader [Zeitwerk::Loader]
30
+ # @return [void]
31
+ def register(cpath, loader)
32
+ cpaths[cpath] = loader
33
+ # We check enabled? because, looking at the C source code, enabling an
34
+ # enabled tracer does not seem to be a simple no-op.
35
+ tracer.enable unless tracer.enabled?
36
+ end
37
+
38
+ # @private
39
+ # @param loader [Zeitwerk::Loader]
40
+ # @return [void]
41
+ def unregister(loader)
42
+ cpaths.delete_if { |_cpath, l| l == loader }
43
+ disable_tracer_if_unneeded
44
+ end
45
+
46
+ def disable_tracer_if_unneeded
47
+ tracer.disable if cpaths.empty?
48
+ end
49
+
50
+ def tracepoint_class_callback(event)
51
+ # If the class is a singleton class, we won't do anything with it so we
52
+ # can bail out immediately. This is several orders of magnitude faster
53
+ # than accessing its name.
54
+ return if event.self.singleton_class?
55
+
56
+ # Note that it makes sense to compute the hash code unconditionally,
57
+ # because the trace point is disabled if cpaths is empty.
58
+ if loader = cpaths.delete(real_mod_name(event.self))
59
+ loader.on_namespace_loaded(event.self)
60
+ disable_tracer_if_unneeded
61
+ end
62
+ end
63
+ end
64
+
65
+ @cpaths = {}
66
+
67
+ # We go through a method instead of defining a block mainly to have a better
68
+ # label when profiling.
69
+ @tracer = TracePoint.new(:class, &method(:tracepoint_class_callback))
70
+ end
71
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zeitwerk
4
+ class Inflector
5
+ # Very basic snake case -> camel case conversion.
6
+ #
7
+ # inflector = Zeitwerk::Inflector.new
8
+ # inflector.camelize("post", ...) # => "Post"
9
+ # inflector.camelize("users_controller", ...) # => "UsersController"
10
+ # inflector.camelize("api", ...) # => "Api"
11
+ #
12
+ # Takes into account hard-coded mappings configured with `inflect`.
13
+ #
14
+ # @param basename [String]
15
+ # @param _abspath [String]
16
+ # @return [String]
17
+ def camelize(basename, _abspath)
18
+ overrides[basename] || basename.split('_').map!(&:capitalize).join
19
+ end
20
+
21
+ # Configures hard-coded inflections:
22
+ #
23
+ # inflector = Zeitwerk::Inflector.new
24
+ # inflector.inflect(
25
+ # "html_parser" => "HTMLParser",
26
+ # "mysql_adapter" => "MySQLAdapter"
27
+ # )
28
+ #
29
+ # inflector.camelize("html_parser", abspath) # => "HTMLParser"
30
+ # inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
31
+ # inflector.camelize("users_controller", abspath) # => "UsersController"
32
+ #
33
+ # @param inflections [{String => String}]
34
+ # @return [void]
35
+ def inflect(inflections)
36
+ overrides.merge!(inflections)
37
+ end
38
+
39
+ private
40
+
41
+ # Hard-coded basename to constant name user maps that override the default
42
+ # inflection logic.
43
+ #
44
+ # @return [{String => String}]
45
+ def overrides
46
+ @overrides ||= {}
47
+ end
48
+ end
49
+ 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 `Opal.modules.hasOwnProperty(path)`
16
+ zeitwerk_original_require(path).tap do |required|
17
+ loader.on_file_autoloaded(path) if required
18
+ end
19
+ else
20
+ loader.on_dir_autoloaded(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_autoloaded(realpath)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,731 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "securerandom"
5
+
6
+ module Zeitwerk
7
+ class Loader
8
+ require_relative "loader/callbacks"
9
+ include Callbacks
10
+ include RealModName
11
+
12
+ # @return [String]
13
+ attr_reader :tag
14
+
15
+ # @return [#camelize]
16
+ attr_accessor :inflector
17
+
18
+ # Absolute paths of the root directories. Stored in a hash to preserve
19
+ # order, easily handle duplicates, and also be able to have a fast lookup,
20
+ # needed for detecting nested paths.
21
+ #
22
+ # "/Users/fxn/blog/app/assets" => true,
23
+ # "/Users/fxn/blog/app/channels" => true,
24
+ # ...
25
+ #
26
+ # This is a private collection maintained by the loader. The public
27
+ # interface for it is `push_dir` and `dirs`.
28
+ #
29
+ # @private
30
+ # @return [{String => true}]
31
+ attr_reader :root_dirs
32
+
33
+ # Absolute paths of files or directories that have to be preloaded.
34
+ #
35
+ # @private
36
+ # @return [<String>]
37
+ attr_reader :preloads
38
+
39
+ # Absolute paths of files, directories, of glob patterns to be totally
40
+ # ignored.
41
+ #
42
+ # @private
43
+ # @return [Set<String>]
44
+ attr_reader :ignored_glob_patterns
45
+
46
+ # The actual collection of absolute file and directory names at the time the
47
+ # ignored glob patterns were expanded. Computed on setup, and recomputed on
48
+ # reload.
49
+ #
50
+ # @private
51
+ # @return [Set<String>]
52
+ attr_reader :ignored_paths
53
+
54
+ # Maps real absolute paths for which an autoload has been set ---and not
55
+ # executed--- to their corresponding parent class or module and constant
56
+ # name.
57
+ #
58
+ # "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
59
+ # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
60
+ # ...
61
+ #
62
+ # @private
63
+ # @return [{String => (Module, Symbol)}]
64
+ attr_reader :autoloads
65
+
66
+ # We keep track of autoloaded directories to remove them from the registry
67
+ # at the end of eager loading.
68
+ #
69
+ # Files are removed as they are autoloaded, but directories need to wait due
70
+ # to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
71
+ #
72
+ # @private
73
+ # @return [<String>]
74
+ attr_reader :autoloaded_dirs
75
+
76
+ # Stores metadata needed for unloading. Its entries look like this:
77
+ #
78
+ # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
79
+ #
80
+ # The cpath as key helps implementing unloadable_cpath? The real file name
81
+ # is stored in order to be able to delete it from $LOADED_FEATURES, and the
82
+ # pair [Module, Symbol] is used to remove_const the constant from the class
83
+ # or module object.
84
+ #
85
+ # If reloading is enabled, this hash is filled as constants are autoloaded
86
+ # or eager loaded. Otherwise, the collection remains empty.
87
+ #
88
+ # @private
89
+ # @return [{String => (String, (Module, Symbol))}]
90
+ attr_reader :to_unload
91
+
92
+ # Maps constant paths of namespaces to arrays of corresponding directories.
93
+ #
94
+ # For example, given this mapping:
95
+ #
96
+ # "Admin" => [
97
+ # "/Users/fxn/blog/app/controllers/admin",
98
+ # "/Users/fxn/blog/app/models/admin",
99
+ # ...
100
+ # ]
101
+ #
102
+ # when `Admin` gets defined we know that it plays the role of a namespace and
103
+ # that its children are spread over those directories. We'll visit them to set
104
+ # up the corresponding autoloads.
105
+ #
106
+ # @private
107
+ # @return [{String => <String>}]
108
+ attr_reader :lazy_subdirs
109
+
110
+ # Absolute paths of files or directories not to be eager loaded.
111
+ #
112
+ # @private
113
+ # @return [Set<String>]
114
+ attr_reader :eager_load_exclusions
115
+
116
+ attr_accessor :vivify_mod_dir
117
+ attr_accessor :vivify_mod_class
118
+
119
+ def initialize
120
+ @initialized_at = Time.now
121
+
122
+ @tag = SecureRandom.hex(3)
123
+ @inflector = Inflector.new
124
+
125
+ @root_dirs = {}
126
+ @preloads = []
127
+ @ignored_glob_patterns = Set.new
128
+ @ignored_paths = Set.new
129
+ @autoloads = {}
130
+ @autoloaded_dirs = []
131
+ @to_unload = {}
132
+ @lazy_subdirs = {}
133
+ @eager_load_exclusions = Set.new
134
+
135
+ @setup = false
136
+ @eager_loaded = false
137
+
138
+ @reloading_enabled = false
139
+
140
+ @vivify_mod_dir = false
141
+ @module_paths
142
+
143
+ Registry.register_loader(self)
144
+ end
145
+
146
+ # Sets a tag for the loader, useful for logging.
147
+ #
148
+ # @return [void]
149
+ def tag=(tag)
150
+ @tag = tag.to_s
151
+ end
152
+
153
+ # Absolute paths of the root directories. This is a read-only collection,
154
+ # please push here via `push_dir`.
155
+ #
156
+ # @return [<String>]
157
+ def dirs
158
+ root_dirs.keys.freeze
159
+ end
160
+
161
+ # Pushes `path` to the list of root directories.
162
+ #
163
+ # Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in
164
+ # the same process already manages that directory or one of its ascendants
165
+ # or descendants.
166
+ #
167
+ # @param path [<String, Pathname>]
168
+ # @raise [Zeitwerk::Error]
169
+ # @return [void]
170
+ def push_dir(path)
171
+ abspath = File.expand_path(path)
172
+ if dir?(abspath)
173
+ raise_if_conflicting_directory(abspath)
174
+ root_dirs[abspath] = true
175
+ else
176
+ warn_string = "Zeitwerk: the root path #{abspath} does not exist, not added"
177
+ `console.warn(warn_string)`
178
+ end
179
+ end
180
+
181
+ # You need to call this method before setup in order to be able to reload.
182
+ # There is no way to undo this, either you want to reload or you don't.
183
+ #
184
+ # @raise [Zeitwerk::Error]
185
+ # @return [void]
186
+ def enable_reloading
187
+ return if @reloading_enabled
188
+
189
+ if @setup
190
+ raise Error, "cannot enable reloading after setup"
191
+ else
192
+ @reloading_enabled = true
193
+ end
194
+ end
195
+
196
+ # @return [Boolean]
197
+ def reloading_enabled?
198
+ @reloading_enabled
199
+ end
200
+
201
+ # Files or directories to be preloaded instead of lazy loaded.
202
+ #
203
+ # @param paths [<String, Pathname, <String, Pathname>>]
204
+ # @return [void]
205
+ def preload(*paths)
206
+ expand_paths(paths).each do |abspath|
207
+ preloads << abspath
208
+ do_preload_abspath(abspath) if @setup
209
+ end
210
+ end
211
+
212
+ # Configure files, directories, or glob patterns to be totally ignored.
213
+ #
214
+ # @param paths [<String, Pathname, <String, Pathname>>]
215
+ # @return [void]
216
+ def ignore(*glob_patterns)
217
+ glob_patterns = expand_paths(glob_patterns)
218
+ ignored_glob_patterns.merge(glob_patterns)
219
+ ignored_paths.merge(expand_glob_patterns(glob_patterns))
220
+ end
221
+
222
+ # Sets autoloads in the root namespace and preloads files, if any.
223
+ #
224
+ # @return [void]
225
+ def setup
226
+ return if @setup
227
+
228
+ actual_root_dirs.each { |root_dir| set_autoloads_in_dir(root_dir, Object) }
229
+ do_preload
230
+
231
+ @setup = true
232
+ end
233
+
234
+ # Removes loaded constants and configured autoloads.
235
+ #
236
+ # The objects the constants stored are no longer reachable through them. In
237
+ # addition, since said objects are normally not referenced from anywhere
238
+ # else, they are eligible for garbage collection, which would effectively
239
+ # unload them.
240
+ #
241
+ # @private
242
+ # @return [void]
243
+ def unload
244
+ # We are going to keep track of the files that were required by our
245
+ # autoloads to later remove them from $LOADED_FEATURES, thus making them
246
+ # loadable by Kernel#require again.
247
+ #
248
+ # Directories are not stored in $LOADED_FEATURES, keeping track of files
249
+ # is enough.
250
+ unloaded_files = Set.new
251
+
252
+ autoloads.each do |realpath, (parent, cname)|
253
+ if parent.autoload?(cname)
254
+ unload_autoload(parent, cname)
255
+ else
256
+ # Could happen if loaded with require_relative. That is unsupported,
257
+ # and the constant path would escape unloadable_cpath? This is just
258
+ # defensive code to clean things up as much as we are able to.
259
+ unload_cref(parent, cname) if cdef?(parent, cname)
260
+ unloaded_files.add(realpath) if ruby?(realpath)
261
+ end
262
+ end
263
+
264
+ to_unload.each_value do |(realpath, (parent, cname))|
265
+ unload_cref(parent, cname) if cdef?(parent, cname)
266
+ unloaded_files.add(realpath) if ruby?(realpath)
267
+ end
268
+
269
+ unless unloaded_files.empty?
270
+ # Bootsnap decorates Kernel#require to speed it up using a cache and
271
+ # this optimization does not check if $LOADED_FEATURES has the file.
272
+ #
273
+ # To make it aware of changes, the gem defines singleton methods in
274
+ # $LOADED_FEATURES:
275
+ #
276
+ # https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
277
+ #
278
+ # Rails applications may depend on bootsnap, so for unloading to work
279
+ # in that setting it is preferable that we restrict our API choice to
280
+ # one of those methods.
281
+ $LOADED_FEATURES.reject! { |file| unloaded_files.member?(file) }
282
+ end
283
+
284
+ autoloads.clear
285
+ autoloaded_dirs.clear
286
+ to_unload.clear
287
+ lazy_subdirs.clear
288
+
289
+ Registry.on_unload(self)
290
+ ExplicitNamespace.unregister(self)
291
+
292
+ @setup = false
293
+ end
294
+
295
+ # Unloads all loaded code, and calls setup again so that the loader is able
296
+ # to pick any changes in the file system.
297
+ #
298
+ # This method is not thread-safe, please see how this can be achieved by
299
+ # client code in the README of the project.
300
+ #
301
+ # @raise [Zeitwerk::Error]
302
+ # @return [void]
303
+ def reload
304
+ if reloading_enabled?
305
+ unload
306
+ recompute_ignored_paths
307
+ setup
308
+ else
309
+ raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
310
+ end
311
+ end
312
+
313
+ # Eager loads all files in the root directories, recursively. Files do not
314
+ # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
315
+ # are not eager loaded. You can opt-out specifically in specific files and
316
+ # directories with `do_not_eager_load`.
317
+ #
318
+ # @return [void]
319
+ def eager_load
320
+ return if @eager_loaded
321
+
322
+ queue = actual_root_dirs.reject { |dir| eager_load_exclusions.member?(dir) }
323
+ queue.map! { |dir| [Object, dir] }
324
+ while to_eager_load = queue.shift
325
+ namespace, dir = to_eager_load
326
+
327
+ ls(dir) do |basename, abspath|
328
+ next if eager_load_exclusions.member?(abspath)
329
+
330
+ if ruby?(abspath)
331
+ if cref = autoloads[File.realpath(abspath)]
332
+ cref[0].const_get(cref[1], false)
333
+ end
334
+ elsif dir?(abspath) && !root_dirs.key?(abspath)
335
+ cname = inflector.camelize(basename, abspath)
336
+ queue << [namespace.const_get(cname, false), abspath]
337
+ end
338
+ end
339
+ end
340
+
341
+ autoloaded_dirs.each do |autoloaded_dir|
342
+ Registry.unregister_autoload(autoloaded_dir)
343
+ end
344
+ autoloaded_dirs.clear
345
+
346
+ @eager_loaded = true
347
+ end
348
+
349
+ # Let eager load ignore the given files or directories. The constants
350
+ # defined in those files are still autoloadable.
351
+ #
352
+ # @param paths [<String, Pathname, <String, Pathname>>]
353
+ # @return [void]
354
+ def do_not_eager_load(*paths)
355
+ eager_load_exclusions.merge(expand_paths(paths))
356
+ end
357
+
358
+ # Says if the given constant path would be unloaded on reload. This
359
+ # predicate returns `false` if reloading is disabled.
360
+ #
361
+ # @param cpath [String]
362
+ # @return [Boolean]
363
+ def unloadable_cpath?(cpath)
364
+ to_unload.key?(cpath)
365
+ end
366
+
367
+ # Returns an array with the constant paths that would be unloaded on reload.
368
+ # This predicate returns an empty array if reloading is disabled.
369
+ #
370
+ # @return [<String>]
371
+ def unloadable_cpaths
372
+ to_unload.keys.freeze
373
+ end
374
+
375
+ # @private
376
+ # @param dir [String]
377
+ # @return [Boolean]
378
+ def manages?(dir)
379
+ dir = dir + "/"
380
+ ignored_paths.each do |ignored_path|
381
+ return false if dir.start_with?(ignored_path + "/")
382
+ end
383
+
384
+ root_dirs.each_key do |root_dir|
385
+ return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
386
+ end
387
+
388
+ false
389
+ end
390
+
391
+ # --- Class methods ---------------------------------------------------------------------------
392
+
393
+ class << self
394
+ # Broadcasts `eager_load` to all loaders.
395
+ #
396
+ # @return [void]
397
+ def eager_load_all
398
+ Registry.loaders.each(&:eager_load)
399
+ end
400
+
401
+ # Returns an array with the absolute paths of the root directories of all
402
+ # registered loaders. This is a read-only collection.
403
+ #
404
+ # @return [<String>]
405
+ def all_dirs
406
+ Registry.loaders.flat_map(&:dirs).freeze
407
+ end
408
+ end
409
+
410
+ # @param dir [String]
411
+ # @param parent [Module]
412
+ # @return [void]
413
+ def set_autoloads_in_dir(dir, parent)
414
+ ls(dir) do |basename, abspath|
415
+ begin
416
+ if ruby?(abspath)
417
+ # basename = basename.slice(-3, 3)
418
+ cname = inflector.camelize(basename, abspath).to_sym
419
+ autoload_file(parent, cname, abspath)
420
+ elsif dir?(abspath)
421
+ # In a Rails application, `app/models/concerns` is a subdirectory of
422
+ # `app/models`, but both of them are root directories.
423
+ #
424
+ # To resolve the ambiguity file name -> constant path this introduces,
425
+ # the `app/models/concerns` directory is totally ignored as a namespace,
426
+ # it counts only as root. The guard checks that.
427
+ unless root_dirs.key?(abspath)
428
+ cname = inflector.camelize(basename, abspath).to_sym
429
+ autoload_subdir(parent, cname, abspath)
430
+ end
431
+ end
432
+ rescue ::NameError => error
433
+ path_type = ruby?(abspath) ? "file" : "directory"
434
+
435
+ raise NameError, <<~MESSAGE
436
+ #{error.message} inferred by #{inflector.class} from #{path_type}
437
+
438
+ #{abspath}
439
+
440
+ Possible ways to address this:
441
+
442
+ * Tell Zeitwerk to ignore this particular #{path_type}.
443
+ * Tell Zeitwerk to ignore one of its parent directories.
444
+ * Rename the #{path_type} to comply with the naming conventions.
445
+ * Modify the inflector to handle this case.
446
+ MESSAGE
447
+ end
448
+ end
449
+ end
450
+
451
+ private # -------------------------------------------------------------------------------------
452
+
453
+ # @return [<String>]
454
+ def actual_root_dirs
455
+ root_dirs.keys.delete_if do |root_dir|
456
+ !dir?(root_dir) || ignored_paths.member?(root_dir)
457
+ end
458
+ end
459
+
460
+ # @param parent [Module]
461
+ # @param cname [Symbol]
462
+ # @param subdir [String]
463
+ # @return [void]
464
+ def autoload_subdir(parent, cname, subdir)
465
+ if autoload_path = autoload_for?(parent, cname)
466
+ cpath = cpath(parent, cname)
467
+ register_explicit_namespace(cpath) if ruby?(autoload_path)
468
+ # We do not need to issue another autoload, the existing one is enough
469
+ # no matter if it is for a file or a directory. Just remember the
470
+ # subdirectory has to be visited if the namespace is used.
471
+ (lazy_subdirs[cpath] ||= []) << subdir
472
+ elsif !cdef?(parent, cname)
473
+ # First time we find this namespace, set an autoload for it.
474
+ (lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
475
+ set_autoload(parent, cname, subdir)
476
+ else
477
+ # For whatever reason the constant that corresponds to this namespace has
478
+ # already been defined, we have to recurse.
479
+ set_autoloads_in_dir(subdir, parent.const_get(cname))
480
+ end
481
+ end
482
+
483
+ # @param parent [Module]
484
+ # @param cname [Symbol]
485
+ # @param file [String]
486
+ # @return [void]
487
+ def autoload_file(parent, cname, file)
488
+ if autoload_path = autoload_for?(parent, cname)
489
+ # First autoload for a Ruby file wins, just ignore subsequent ones.
490
+ if ruby?(autoload_path)
491
+ # "file #{file} is ignored because #{autoload_path} has precedence"
492
+ else
493
+ promote_namespace_from_implicit_to_explicit(
494
+ dir: autoload_path,
495
+ file: file,
496
+ parent: parent,
497
+ cname: cname
498
+ )
499
+ end
500
+ elsif cdef?(parent, cname)
501
+ # "file #{file} is ignored because #{cpath(parent, cname)} is already defined"
502
+ else
503
+ set_autoload(parent, cname, file)
504
+ end
505
+ end
506
+
507
+ # @param dir [String] directory that would have autovivified a module
508
+ # @param file [String] the file where the namespace is explictly defined
509
+ # @param parent [Module]
510
+ # @param cname [Symbol]
511
+ # @return [void]
512
+ def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
513
+ autoloads.delete(dir)
514
+ Registry.unregister_autoload(dir)
515
+
516
+ set_autoload(parent, cname, file)
517
+ register_explicit_namespace(cpath(parent, cname))
518
+ end
519
+
520
+ # @param parent [Module]
521
+ # @param cname [Symbol]
522
+ # @param abspath [String]
523
+ # @return [void]
524
+ def set_autoload(parent, cname, abspath)
525
+ # $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the
526
+ # real path to be able to delete it from $LOADED_FEATURES on unload, and to
527
+ # be able to do a lookup later in Kernel#require for manual require calls.
528
+ realpath = File.realpath(abspath)
529
+ parent.autoload(cname, realpath)
530
+
531
+ autoloads[realpath] = [parent, cname]
532
+ Registry.register_autoload(self, realpath)
533
+
534
+ # See why in the documentation of Zeitwerk::Registry.inceptions.
535
+ unless parent.autoload?(cname)
536
+ Registry.register_inception(cpath(parent, cname), realpath, self)
537
+ end
538
+ end
539
+
540
+ # @param parent [Module]
541
+ # @param cname [Symbol]
542
+ # @return [String, nil]
543
+ def autoload_for?(parent, cname)
544
+ strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
545
+ end
546
+
547
+ # The autoload? predicate takes into account the ancestor chain of the
548
+ # receiver, like const_defined? and other methods in the constants API do.
549
+ #
550
+ # For example, given
551
+ #
552
+ # class A
553
+ # autoload :X, "x.rb"
554
+ # end
555
+ #
556
+ # class B < A
557
+ # end
558
+ #
559
+ # B.autoload?(:X) returns "x.rb".
560
+ #
561
+ # We need a way to strictly check in parent ignoring ancestors.
562
+ #
563
+ # @param parent [Module]
564
+ # @param cname [Symbol]
565
+ # @return [String, nil]
566
+ if method(:autoload?).arity == 1
567
+ def strict_autoload_path(parent, cname)
568
+ parent.autoload?(cname) if cdef?(parent, cname)
569
+ end
570
+ else
571
+ def strict_autoload_path(parent, cname)
572
+ parent.autoload?(cname, false)
573
+ end
574
+ end
575
+
576
+ # This method is called this way because I prefer `preload` to be the method
577
+ # name to configure preloads in the public interface.
578
+ #
579
+ # @return [void]
580
+ def do_preload
581
+ preloads.each do |abspath|
582
+ do_preload_abspath(abspath)
583
+ end
584
+ end
585
+
586
+ # @param abspath [String]
587
+ # @return [void]
588
+ def do_preload_abspath(abspath)
589
+ if ruby?(abspath)
590
+ do_preload_file(abspath)
591
+ elsif dir?(abspath)
592
+ do_preload_dir(abspath)
593
+ end
594
+ end
595
+
596
+ # @param dir [String]
597
+ # @return [void]
598
+ def do_preload_dir(dir)
599
+ ls(dir) do |_basename, abspath|
600
+ do_preload_abspath(abspath)
601
+ end
602
+ end
603
+
604
+ # @param file [String]
605
+ # @return [Boolean]
606
+ def do_preload_file(file)
607
+ require file
608
+ end
609
+
610
+ # @param parent [Module]
611
+ # @param cname [Symbol]
612
+ # @return [String]
613
+ def cpath(parent, cname)
614
+ parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
615
+ end
616
+
617
+ # @param dir [String]
618
+ # @yieldparam path [String, String]
619
+ # @return [void]
620
+ def ls(dir)
621
+ # `console.log("dir:", dir)`
622
+ outer_ls = false
623
+ # cache the Opal.modules keys array for subsequent ls calls during setup
624
+ if !@module_paths
625
+ @module_paths = `Object.keys(Opal.modules)`
626
+ outer_ls = true
627
+ end
628
+ visited_abspaths = `{}`
629
+ dir_first_char = dir[0]
630
+ path_start = dir.size + 1
631
+ path_parts = `[]`
632
+ basename = `''`
633
+ @module_paths.each do |abspath|
634
+ %x{
635
+ if (abspath[0] === dir_first_char) {
636
+ if (!abspath.startsWith(dir)) { #{next} }
637
+ path_parts = abspath.slice(path_start).split('/');
638
+ basename = path_parts[0];
639
+ abspath = dir + '/' + basename;
640
+ if (visited_abspaths.hasOwnProperty(abspath)) { #{next} }
641
+ visited_abspaths[abspath] = true;
642
+ // console.log("basename:", basename, "abspath:", abspath);
643
+ #{yield basename, abspath unless ignored_paths.member?(abspath)}
644
+ }
645
+ }
646
+ end
647
+ # remove cache, because Opal.modules may change after setup
648
+ if outer_ls
649
+ @module_paths = nil
650
+ end
651
+ end
652
+
653
+ # @param path [String]
654
+ # @return [Boolean]
655
+ def ruby?(abspath)
656
+ `Opal.modules.hasOwnProperty(abspath)`
657
+ end
658
+
659
+ # @param path [String]
660
+ # @return [Boolean]
661
+ def dir?(path)
662
+ dir_path = path + '/'
663
+ module_paths = if @module_paths # possibly set by ls
664
+ @module_paths
665
+ else
666
+ `Object.keys(Opal.modules)`
667
+ end
668
+ path_first = `path[0]`
669
+ module_paths.each do |m_path|
670
+ %x{
671
+ if (m_path[0] !== path_first) { #{ next } }
672
+ if (m_path.startsWith(dir_path)) { #{return true} }
673
+ }
674
+ end
675
+ return false
676
+ end
677
+
678
+ # @param paths [<String, Pathname, <String, Pathname>>]
679
+ # @return [<String>]
680
+ def expand_paths(paths)
681
+ paths.flatten.map! { |path| File.expand_path(path) }
682
+ end
683
+
684
+ # @param glob_patterns [<String>]
685
+ # @return [<String>]
686
+ def expand_glob_patterns(glob_patterns)
687
+ # Note that Dir.glob works with regular file names just fine. That is,
688
+ # glob patterns technically need no wildcards.
689
+ glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
690
+ end
691
+
692
+ # @return [void]
693
+ def recompute_ignored_paths
694
+ ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
695
+ end
696
+
697
+ def cdef?(parent, cname)
698
+ parent.const_defined?(cname, false)
699
+ end
700
+
701
+ def register_explicit_namespace(cpath)
702
+ ExplicitNamespace.register(cpath, self)
703
+ end
704
+
705
+ def raise_if_conflicting_directory(dir)
706
+ Registry.loaders.each do |loader|
707
+ if loader != self && loader.manages?(dir)
708
+ require "pp"
709
+ raise Error,
710
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
711
+ " which is already managed by\n\n#{loader.pretty_inspect}\n"
712
+ EOS
713
+ end
714
+ end
715
+ end
716
+
717
+ # @param parent [Module]
718
+ # @param cname [Symbol]
719
+ # @return [void]
720
+ def unload_autoload(parent, cname)
721
+ parent.send(:remove_const, cname)
722
+ end
723
+
724
+ # @param parent [Module]
725
+ # @param cname [Symbol]
726
+ # @return [void]
727
+ def unload_cref(parent, cname)
728
+ parent.send(:remove_const, cname)
729
+ end
730
+ end
731
+ end
@@ -0,0 +1,61 @@
1
+ module Zeitwerk::Loader::Callbacks
2
+ include Zeitwerk::RealModName
3
+
4
+ # Invoked from our decorated Kernel#require when a managed file is autoloaded.
5
+ #
6
+ # @private
7
+ # @param file [String]
8
+ # @return [void]
9
+ def on_file_autoloaded(file)
10
+ cref = autoloads.delete(file)
11
+ to_unload[cpath(*cref)] = [file, cref] if reloading_enabled?
12
+ Zeitwerk::Registry.unregister_autoload(file)
13
+
14
+ # "constant #{cpath(*cref)} loaded from file #{file}" if cdef?(*cref)
15
+ if !cdef?(*cref)
16
+ raise Zeitwerk::NameError, "expected file #{file} to define constant #{cpath(*cref)}, but didn't"
17
+ end
18
+ end
19
+
20
+ # Invoked from our decorated Kernel#require when a managed directory is
21
+ # autoloaded.
22
+ #
23
+ # @private
24
+ # @param dir [String]
25
+ # @return [void]
26
+ def on_dir_autoloaded(dir)
27
+ if cref = autoloads.delete(dir)
28
+ autovivified_module = if vivify_mod_dir && dir.start_with?(vivify_mod_dir)
29
+ cref[0].const_set(cref[1], vivify_mod_class.new)
30
+ else
31
+ cref[0].const_set(cref[1], Module.new)
32
+ end
33
+ # "module #{autovivified_module.name} autovivified from directory #{dir}"
34
+
35
+ to_unload[autovivified_module.name] = [dir, cref] if reloading_enabled?
36
+
37
+ # We don't unregister `dir` in the registry because concurrent threads
38
+ # wouldn't find a loader associated to it in Kernel#require and would
39
+ # try to require the directory. Instead, we are going to keep track of
40
+ # these to be able to unregister later if eager loading.
41
+ autoloaded_dirs << dir
42
+
43
+ on_namespace_loaded(autovivified_module)
44
+ end
45
+ end
46
+
47
+ # Invoked when a class or module is created or reopened, either from the
48
+ # tracer or from module autovivification. If the namespace has matching
49
+ # subdirectories, we descend into them now.
50
+ #
51
+ # @private
52
+ # @param namespace [Module]
53
+ # @return [void]
54
+ def on_namespace_loaded(namespace)
55
+ if subdirs = lazy_subdirs.delete(real_mod_name(namespace))
56
+ subdirs.each do |subdir|
57
+ set_autoloads_in_dir(subdir, namespace)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,21 @@
1
+ module Zeitwerk::RealModName
2
+ UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name)
3
+ private_constant :UNBOUND_METHOD_MODULE_NAME
4
+
5
+ # Returns the real name of the class or module, as set after the first
6
+ # constant to which it was assigned (or nil).
7
+ #
8
+ # The name method can be overridden, hence the indirection in this method.
9
+ #
10
+ # @param mod [Class, Module]
11
+ # @return [String, nil]
12
+ if UnboundMethod.method_defined?(:bind_call)
13
+ def real_mod_name(mod)
14
+ UNBOUND_METHOD_MODULE_NAME.bind_call(mod)
15
+ end
16
+ else
17
+ def real_mod_name(mod)
18
+ UNBOUND_METHOD_MODULE_NAME.bind(mod).call
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,123 @@
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
+ # Maps real paths to the loaders responsible for them.
14
+ #
15
+ # This information is used by our decorated `Kernel#require` to be able to
16
+ # invoke callbacks and autovivify modules.
17
+ #
18
+ # @private
19
+ # @return [{String => Zeitwerk::Loader}]
20
+ attr_reader :autoloads
21
+
22
+ # This hash table addresses an edge case in which an autoload is ignored.
23
+ #
24
+ # For example, let's suppose we want to autoload in a gem like this:
25
+ #
26
+ # # lib/my_gem.rb
27
+ # loader = Zeitwerk::Loader.new
28
+ # loader.push_dir(__dir__)
29
+ # loader.setup
30
+ #
31
+ # module MyGem
32
+ # end
33
+ #
34
+ # if you require "my_gem", as Bundler would do, this happens while setting
35
+ # up autoloads:
36
+ #
37
+ # 1. Object.autoload?(:MyGem) returns `nil` because the autoload for
38
+ # the constant is issued by Zeitwerk while the same file is being
39
+ # required.
40
+ # 2. The constant `MyGem` is undefined while setup runs.
41
+ #
42
+ # Therefore, a directory `lib/my_gem` would autovivify a module according to
43
+ # the existing information. But that would be wrong.
44
+ #
45
+ # To overcome this fundamental limitation, we keep track of the constant
46
+ # paths that are in this situation ---in the example above, "MyGem"--- and
47
+ # take this collection into account for the autovivification logic.
48
+ #
49
+ # Note that you cannot generally address this by moving the setup code
50
+ # below the constant definition, because we want libraries to be able to
51
+ # use managed constants in the module body:
52
+ #
53
+ # module MyGem
54
+ # include MyConcern
55
+ # end
56
+ #
57
+ # @private
58
+ # @return [{String => (String, Zeitwerk::Loader)}]
59
+ attr_reader :inceptions
60
+
61
+ # Registers a loader.
62
+ #
63
+ # @private
64
+ # @param loader [Zeitwerk::Loader]
65
+ # @return [void]
66
+ def register_loader(loader)
67
+ loaders << loader
68
+ end
69
+
70
+ # @private
71
+ # @param loader [Zeitwerk::Loader]
72
+ # @param realpath [String]
73
+ # @return [void]
74
+ def register_autoload(loader, realpath)
75
+ autoloads[realpath] = loader
76
+ end
77
+
78
+ # @private
79
+ # @param realpath [String]
80
+ # @return [void]
81
+ def unregister_autoload(realpath)
82
+ autoloads.delete(realpath)
83
+ end
84
+
85
+ # @private
86
+ # @param cpath [String]
87
+ # @param realpath [String]
88
+ # @param loader [Zeitwerk::Loader]
89
+ # @return [void]
90
+ def register_inception(cpath, realpath, loader)
91
+ inceptions[cpath] = [realpath, loader]
92
+ end
93
+
94
+ # @private
95
+ # @param cpath [String]
96
+ # @return [String, nil]
97
+ def inception?(cpath)
98
+ if pair = inceptions[cpath]
99
+ pair.first
100
+ end
101
+ end
102
+
103
+ # @private
104
+ # @param path [String]
105
+ # @return [Zeitwerk::Loader, nil]
106
+ def loader_for(path)
107
+ autoloads[path]
108
+ end
109
+
110
+ # @private
111
+ # @param loader [Zeitwerk::Loader]
112
+ # @return [void]
113
+ def on_unload(loader)
114
+ autoloads.delete_if { |_path, object| object == loader }
115
+ inceptions.delete_if { |_cpath, (_path, object)| object == loader }
116
+ end
117
+ end
118
+
119
+ @loaders = []
120
+ @autoloads = {}
121
+ @inceptions = {}
122
+ end
123
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opal-zeitwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Biedermann
@@ -9,7 +9,21 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
  date: 2019-11-04 00:00:00.000000000 Z
12
- dependencies: []
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: opal
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.0
13
27
  description: |2
14
28
  A port of the Zeitwerk autoloader to Opal.
15
29
  Zeitwerk implements constant autoloading with Ruby semantics. Each gem
@@ -22,6 +36,17 @@ extra_rdoc_files: []
22
36
  files:
23
37
  - MIT-LICENSE
24
38
  - README.md
39
+ - lib/opal-zeitwerk.rb
40
+ - lib/opal/zeitwerk/version.rb
41
+ - opal/zeitwerk.rb
42
+ - opal/zeitwerk/error.rb
43
+ - opal/zeitwerk/explicit_namespace.rb
44
+ - opal/zeitwerk/inflector.rb
45
+ - opal/zeitwerk/kernel.rb
46
+ - opal/zeitwerk/loader.rb
47
+ - opal/zeitwerk/loader/callbacks.rb
48
+ - opal/zeitwerk/real_mod_name.rb
49
+ - opal/zeitwerk/registry.rb
25
50
  homepage: https://github.com/isomorfeus/opal-zeitwerk
26
51
  licenses:
27
52
  - MIT