opal-zeitwerk 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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