opal-zeitwerk 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
- module Opal
3
- module Zeitwerk
4
- VERSION = "0.3.0"
5
- end
6
- end
1
+ # frozen_string_literal: true
2
+ module Opal
3
+ module Zeitwerk
4
+ VERSION = "0.4.0"
5
+ end
6
+ end
data/lib/opal-zeitwerk.rb CHANGED
@@ -1,4 +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
1
+ require 'opal'
2
+ require 'opal/zeitwerk/version'
3
+
4
+ Opal.append_path File.expand_path(File.join(File.dirname(__FILE__), '..', 'opal')).untaint
@@ -1,10 +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
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
@@ -1,71 +1,78 @@
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
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
+ # @sig Hash[String, Zeitwerk::Loader]
18
+ attr_reader :cpaths
19
+
20
+ # @private
21
+ # @sig TracePoint
22
+ attr_reader :tracer
23
+
24
+ # Asserts `cpath` corresponds to an explicit namespace for which `loader`
25
+ # is responsible.
26
+ #
27
+ # @private
28
+ # @sig (String, Zeitwerk::Loader) -> void
29
+ def register(cpath, loader)
30
+ cpaths[cpath] = loader
31
+ # We check enabled? because, looking at the C source code, enabling an
32
+ # enabled tracer does not seem to be a simple no-op.
33
+ tracer.enable unless tracer.enabled?
34
+ end
35
+
36
+ # @private
37
+ # @sig (Zeitwerk::Loader) -> void
38
+ def unregister_loader(loader)
39
+ cpaths.delete_if { |_cpath, l| l == loader }
40
+ disable_tracer_if_unneeded
41
+ end
42
+
43
+ private
44
+
45
+ # @sig () -> void
46
+ def disable_tracer_if_unneeded
47
+ tracer.disable if cpaths.empty?
48
+ end
49
+
50
+ # @sig (TracePoint) -> void
51
+ def tracepoint_class_callback(event)
52
+ # If the class is a singleton class, we won't do anything with it so we
53
+ # can bail out immediately. This is several orders of magnitude faster
54
+ # than accessing its name.
55
+ return if event.self.singleton_class?
56
+
57
+ # It might be tempting to return if name.nil?, to avoid the computation
58
+ # of a hash code and delete call. But Ruby does not trigger the :class
59
+ # event on Class.new or Module.new, so that would incur in an extra call
60
+ # for nothing.
61
+ #
62
+ # On the other hand, if we were called, cpaths is not empty. Otherwise
63
+ # the tracer is disabled. So we do need to go ahead with the hash code
64
+ # computation and delete call.
65
+ if loader = cpaths.delete(real_mod_name(event.self))
66
+ loader.on_namespace_loaded(event.self)
67
+ disable_tracer_if_unneeded
68
+ end
69
+ end
70
+ end
71
+
72
+ @cpaths = {}
73
+
74
+ # We go through a method instead of defining a block mainly to have a better
75
+ # label when profiling.
76
+ @tracer = TracePoint.new(:class, &method(:tracepoint_class_callback))
77
+ end
78
+ end
@@ -0,0 +1,15 @@
1
+ module Zeitwerk
2
+ class GemInflector < Inflector
3
+ # @sig (String) -> void
4
+ def initialize(root_file)
5
+ namespace = File.basename(root_file, ".rb")
6
+ lib_dir = File.dirname(root_file)
7
+ @version_file = File.join(lib_dir, namespace, "version")
8
+ end
9
+
10
+ # @sig (String, String) -> String
11
+ def camelize(basename, abspath)
12
+ abspath == @version_file ? "VERSION" : super
13
+ end
14
+ end
15
+ end
@@ -1,47 +1,44 @@
1
- module Zeitwerk
2
- class Inflector
3
- # Very basic snake case -> camel case conversion.
4
- #
5
- # inflector = Zeitwerk::Inflector.new
6
- # inflector.camelize("post", ...) # => "Post"
7
- # inflector.camelize("users_controller", ...) # => "UsersController"
8
- # inflector.camelize("api", ...) # => "Api"
9
- #
10
- # Takes into account hard-coded mappings configured with `inflect`.
11
- #
12
- # @param basename [String]
13
- # @param _abspath [String]
14
- # @return [String]
15
- def camelize(basename, _abspath)
16
- overrides[basename] || basename.split('_').map!(&:capitalize).join
17
- end
18
-
19
- # Configures hard-coded inflections:
20
- #
21
- # inflector = Zeitwerk::Inflector.new
22
- # inflector.inflect(
23
- # "html_parser" => "HTMLParser",
24
- # "mysql_adapter" => "MySQLAdapter"
25
- # )
26
- #
27
- # inflector.camelize("html_parser", abspath) # => "HTMLParser"
28
- # inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
29
- # inflector.camelize("users_controller", abspath) # => "UsersController"
30
- #
31
- # @param inflections [{String => String}]
32
- # @return [void]
33
- def inflect(inflections)
34
- overrides.merge!(inflections)
35
- end
36
-
37
- private
38
-
39
- # Hard-coded basename to constant name user maps that override the default
40
- # inflection logic.
41
- #
42
- # @return [{String => String}]
43
- def overrides
44
- @overrides ||= {}
45
- end
46
- end
47
- end
1
+ module Zeitwerk
2
+ class Inflector
3
+ # Very basic snake case -> camel case conversion.
4
+ #
5
+ # inflector = Zeitwerk::Inflector.new
6
+ # inflector.camelize("post", ...) # => "Post"
7
+ # inflector.camelize("users_controller", ...) # => "UsersController"
8
+ # inflector.camelize("api", ...) # => "Api"
9
+ #
10
+ # Takes into account hard-coded mappings configured with `inflect`.
11
+ #
12
+ # @sig (String, String) -> String
13
+ def camelize(basename, _abspath)
14
+ overrides[basename] || basename.split('_').map!(&:capitalize).join
15
+ end
16
+
17
+ # Configures hard-coded inflections:
18
+ #
19
+ # inflector = Zeitwerk::Inflector.new
20
+ # inflector.inflect(
21
+ # "html_parser" => "HTMLParser",
22
+ # "mysql_adapter" => "MySQLAdapter"
23
+ # )
24
+ #
25
+ # inflector.camelize("html_parser", abspath) # => "HTMLParser"
26
+ # inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
27
+ # inflector.camelize("users_controller", abspath) # => "UsersController"
28
+ #
29
+ # @sig (Hash[String, String]) -> void
30
+ def inflect(inflections)
31
+ overrides.merge!(inflections)
32
+ end
33
+
34
+ private
35
+
36
+ # Hard-coded basename to constant name user maps that override the default
37
+ # inflection logic.
38
+ #
39
+ # @sig () -> Hash[String, String]
40
+ def overrides
41
+ @overrides ||= {}
42
+ end
43
+ end
44
+ end
@@ -1,32 +1,64 @@
1
- module Kernel
2
- module_function
3
-
4
- # We cannot decorate with prepend + super because Kernel has already been
5
- # included in Object, and changes in ancestors don't get propagated into
6
- # already existing ancestor chains.
7
- alias_method :zeitwerk_original_require, :require
8
-
9
- # @param path [String]
10
- # @return [Boolean]
11
- def require(path)
12
- path = `Opal.normalize(#{path})`
13
- if loader = Zeitwerk::Registry.loader_for(path)
14
- if `Opal.modules.hasOwnProperty(path)`
15
- zeitwerk_original_require(path).tap do |required|
16
- loader.on_file_autoloaded(path) if required
17
- end
18
- else
19
- loader.on_dir_autoloaded(path)
20
- end
21
- else
22
- zeitwerk_original_require(path).tap do |required|
23
- if required
24
- realpath = $LOADED_FEATURES.last
25
- if loader = Zeitwerk::Registry.loader_for(realpath)
26
- loader.on_file_autoloaded(realpath)
27
- end
28
- end
29
- end
30
- end
31
- end
32
- end
1
+ module Kernel
2
+ module_function
3
+
4
+ # Zeitwerk's main idea is to define autoloads for project constants, and then
5
+ # intercept them when triggered in this thin `Kernel#require` wrapper.
6
+ #
7
+ # That allows us to complete the circle, invoke callbacks, autovivify modules,
8
+ # define autoloads for just autoloaded namespaces, update internal state, etc.
9
+ #
10
+ # On the other hand, if you publish a new version of a gem that is now managed
11
+ # by Zeitwerk, client code can reference directly your classes and modules and
12
+ # should not require anything. But if someone has legacy require calls around,
13
+ # they will work as expected, and in a compatible way. This feature is by now
14
+ # EXPERIMENTAL and UNDOCUMENTED.
15
+ #
16
+ # We cannot decorate with prepend + super because Kernel has already been
17
+ # included in Object, and changes in ancestors don't get propagated into
18
+ # already existing ancestor chains on Ruby < 3.0.
19
+ alias_method :zeitwerk_original_require, :require
20
+
21
+ # @sig (String) -> true | false
22
+ def require(path)
23
+ path = `Opal.normalize(#{path})`
24
+ if loader = Zeitwerk::Registry.loader_for(path)
25
+ if `Opal.modules.hasOwnProperty(path)`
26
+ required = zeitwerk_original_require(path)
27
+ loader.on_file_autoloaded(path) if required
28
+ required
29
+ else
30
+ loader.on_dir_autoloaded(path)
31
+ true
32
+ end
33
+ else
34
+ required = zeitwerk_original_require(path)
35
+ if required
36
+ realpath = $LOADED_FEATURES.last
37
+ if loader = Zeitwerk::Registry.loader_for(realpath)
38
+ loader.on_file_autoloaded(realpath)
39
+ end
40
+ end
41
+ required
42
+ end
43
+ end
44
+
45
+ # By now, I have seen no way so far to decorate require_relative.
46
+ #
47
+ # For starters, at least in CRuby, require_relative does not delegate to
48
+ # require. Both require and require_relative delegate the bulk of their work
49
+ # to an internal C function called rb_require_safe. So, our require wrapper is
50
+ # not executed.
51
+ #
52
+ # On the other hand, we cannot use the aliasing technique above because
53
+ # require_relative receives a path relative to the directory of the file in
54
+ # which the call is performed. If a wrapper here invoked the original method,
55
+ # Ruby would resolve the relative path taking lib/zeitwerk as base directory.
56
+ #
57
+ # A workaround could be to extract the base directory from caller_locations,
58
+ # but what if someone else decorated require_relative before us? You can't
59
+ # really know with certainty where's the original call site in the stack.
60
+ #
61
+ # However, the main use case for require_relative is to load files from your
62
+ # own project. Projects managed by Zeitwerk don't do this for files managed by
63
+ # Zeitwerk, precisely.
64
+ end
@@ -1,58 +1,88 @@
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.new("expected file #{file} to define constant #{cpath(*cref)}, but didn't", cref.last)
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 = cref[0].const_set(cref[1], Module.new)
29
-
30
- # "module #{autovivified_module.name} autovivified from directory #{dir}"
31
-
32
- to_unload[autovivified_module.name] = [dir, cref] if reloading_enabled?
33
-
34
- # We don't unregister `dir` in the registry because concurrent threads
35
- # wouldn't find a loader associated to it in Kernel#require and would
36
- # try to require the directory. Instead, we are going to keep track of
37
- # these to be able to unregister later if eager loading.
38
- autoloaded_dirs << dir
39
-
40
- on_namespace_loaded(autovivified_module)
41
- end
42
- end
43
-
44
- # Invoked when a class or module is created or reopened, either from the
45
- # tracer or from module autovivification. If the namespace has matching
46
- # subdirectories, we descend into them now.
47
- #
48
- # @private
49
- # @param namespace [Module]
50
- # @return [void]
51
- def on_namespace_loaded(namespace)
52
- if subdirs = lazy_subdirs.delete(real_mod_name(namespace))
53
- subdirs.each do |subdir|
54
- set_autoloads_in_dir(subdir, namespace)
55
- end
56
- end
57
- end
58
- end
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
+ # @sig (String) -> void
8
+ def on_file_autoloaded(file)
9
+ cref = autoloads.delete(file)
10
+ cpath = cpath(*cref)
11
+
12
+ # If reloading is enabled, we need to put this constant for unloading
13
+ # regardless of what cdef? says. In Ruby < 3.1 the internal state is not
14
+ # fully cleared. Module#constants still includes it, and you need to
15
+ # remove_const. See https://github.com/ruby/ruby/pull/4715.
16
+ to_unload[cpath] = [file, cref] if reloading_enabled?
17
+ Zeitwerk::Registry.unregister_autoload(file)
18
+
19
+ if cdef?(*cref)
20
+ run_on_load_callbacks(cpath, cget(*cref), file) unless on_load_callbacks.empty?
21
+ else
22
+ raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath}, but didn't", cref.last)
23
+ end
24
+ end
25
+
26
+ # Invoked from our decorated Kernel#require when a managed directory is
27
+ # autoloaded.
28
+ #
29
+ # @private
30
+ # @sig (String) -> void
31
+ def on_dir_autoloaded(dir)
32
+ # Module#autoload does not serialize concurrent requires, and we handle
33
+ # directories ourselves, so the callback needs to account for concurrency.
34
+ #
35
+ # Multi-threading would introduce a race condition here in which thread t1
36
+ # autovivifies the module, and while autoloads for its children are being
37
+ # set, thread t2 autoloads the same namespace.
38
+ #
39
+ # Without the mutex and subsequent delete call, t2 would reset the module.
40
+ # That not only would reassign the constant (undesirable per se) but, worse,
41
+ # the module object created by t2 wouldn't have any of the autoloads for its
42
+ # children, since t1 would have correctly deleted its lazy_subdirs entry.
43
+
44
+ # Well, on Opal no mutexes.
45
+ if cref = autoloads.delete(dir)
46
+ autovivified_module = cref[0].const_set(cref[1], Module.new)
47
+ cpath = autovivified_module.name
48
+
49
+ to_unload[cpath] = [dir, cref] if reloading_enabled?
50
+
51
+ # We don't unregister `dir` in the registry because concurrent threads
52
+ # wouldn't find a loader associated to it in Kernel#require and would
53
+ # try to require the directory. Instead, we are going to keep track of
54
+ # these to be able to unregister later if eager loading.
55
+ autoloaded_dirs << dir
56
+
57
+ on_namespace_loaded(autovivified_module)
58
+
59
+ run_on_load_callbacks(cpath, autovivified_module, dir) unless on_load_callbacks.empty?
60
+ end
61
+ end
62
+
63
+ # Invoked when a class or module is created or reopened, either from the
64
+ # tracer or from module autovivification. If the namespace has matching
65
+ # subdirectories, we descend into them now.
66
+ #
67
+ # @private
68
+ # @sig (Module) -> void
69
+ def on_namespace_loaded(namespace)
70
+ if subdirs = lazy_subdirs.delete(real_mod_name(namespace))
71
+ subdirs.each do |subdir|
72
+ set_autoloads_in_dir(subdir, namespace)
73
+ end
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ # @sig (String, Object) -> void
80
+ def run_on_load_callbacks(cpath, value, abspath)
81
+ # Order matters. If present, run the most specific one.
82
+ callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)
83
+ callbacks&.each { |c| c.call(value, abspath) }
84
+
85
+ callbacks = on_load_callbacks[:ANY]
86
+ callbacks&.each { |c| c.call(cpath, value, abspath) }
87
+ end
88
+ end