opal-zeitwerk 0.0.1 → 0.0.2

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 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