require-hooks 0.2.3 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/lib/require-hooks/api.rb +63 -29
- data/lib/require-hooks/mode/bootsnap.rb +14 -1
- data/lib/require-hooks/mode/kernel_patch.rb +4 -30
- data/lib/require-hooks/mode/load_iseq.rb +19 -11
- data/lib/require-hooks/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36e7f23cef600d120d9139555f3e3ef4b8bd46b9929387b99dce99280108615c
|
|
4
|
+
data.tar.gz: c89a896109b87d81d2e1a7690398bc337b6723c2ae006c5a2a0f0c7a8e01c52a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4af96e3f1d117b10e7cedc4cb564e47768ac5eb44e8f83e666e90b9f98178fd827a19641caa0f2dd3e674736ec7aa0982e11d184fa780e46fc89a0c399098f00
|
|
7
|
+
data.tar.gz: c0c60d8e40d22743aa24c671b88989c6aef61c58d39cdefe39e26c73905fc08286d0fb68d09e409e0762e8ccbfe5334926d7c35321f2442bf4158e5e8dda0d1e
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## master
|
|
4
4
|
|
|
5
|
+
## 0.3.0 (2026-04-22)
|
|
6
|
+
|
|
7
|
+
- Fix the order of around hooks execution (after part) when using `#load_iseq` driven hooks.
|
|
8
|
+
|
|
9
|
+
- Improve `Kernel#require` patch performance.
|
|
10
|
+
|
|
11
|
+
- Reduce context object creation and use a single object when only one context defined.
|
|
12
|
+
|
|
5
13
|
## 0.2.3 (2026-01-13)
|
|
6
14
|
|
|
7
15
|
- Gem metadata fixes.
|
data/lib/require-hooks/api.rb
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RequireHooks
|
|
4
|
-
@@around_load = []
|
|
5
|
-
@@source_transform = []
|
|
6
|
-
@@hijack_load = []
|
|
7
|
-
|
|
8
4
|
class Context
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
attr_reader :around_load, :source_transform, :hijack_load,
|
|
6
|
+
:patterns, :exclude_patterns
|
|
7
|
+
|
|
8
|
+
def initialize(patterns: nil, exclude_patterns: nil)
|
|
9
|
+
@patterns = patterns.freeze
|
|
10
|
+
@exclude_patterns = exclude_patterns.freeze
|
|
11
|
+
|
|
12
|
+
@around_load = []
|
|
13
|
+
@source_transform = []
|
|
14
|
+
@hijack_load = []
|
|
15
|
+
|
|
16
|
+
@empty = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_key
|
|
20
|
+
[patterns, exclude_patterns]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def match?(path)
|
|
24
|
+
return false unless !patterns || patterns.any? { |pattern| File.fnmatch?(pattern, path) }
|
|
25
|
+
return false if exclude_patterns&.any? { |pattern| File.fnmatch?(pattern, path) }
|
|
26
|
+
true
|
|
13
27
|
end
|
|
14
28
|
|
|
15
29
|
def empty?
|
|
16
|
-
@
|
|
30
|
+
return @empty unless @empty.nil?
|
|
31
|
+
@empty = @around_load.empty? && @source_transform.empty? && @hijack_load.empty?
|
|
17
32
|
end
|
|
18
33
|
|
|
19
34
|
def source_transform?
|
|
@@ -57,6 +72,10 @@ module RequireHooks
|
|
|
57
72
|
end
|
|
58
73
|
end
|
|
59
74
|
|
|
75
|
+
@@default_context = Context.new
|
|
76
|
+
@@noop_context = Context.new
|
|
77
|
+
@@contexts = {}
|
|
78
|
+
|
|
60
79
|
class << self
|
|
61
80
|
attr_accessor :print_warnings
|
|
62
81
|
|
|
@@ -69,7 +88,13 @@ module RequireHooks
|
|
|
69
88
|
# block.call.tap { puts "Loaded #{path}" }
|
|
70
89
|
# end
|
|
71
90
|
def around_load(patterns: nil, exclude_patterns: nil, &block)
|
|
72
|
-
@@
|
|
91
|
+
@@default_context = nil
|
|
92
|
+
ctx = Context.new(patterns: patterns, exclude_patterns: exclude_patterns)
|
|
93
|
+
|
|
94
|
+
@@contexts[ctx.to_key] ||= ctx
|
|
95
|
+
@@contexts[ctx.to_key].around_load << block
|
|
96
|
+
|
|
97
|
+
@@default_context = @@contexts.values.first if @@contexts.size == 1
|
|
73
98
|
end
|
|
74
99
|
|
|
75
100
|
# Define hooks to perform source-to-source transformations.
|
|
@@ -83,7 +108,13 @@ module RequireHooks
|
|
|
83
108
|
# "# frozen_string_literal: true\n#{source}"
|
|
84
109
|
# end
|
|
85
110
|
def source_transform(patterns: nil, exclude_patterns: nil, &block)
|
|
86
|
-
@@
|
|
111
|
+
@@default_context = nil
|
|
112
|
+
ctx = Context.new(patterns: patterns, exclude_patterns: exclude_patterns)
|
|
113
|
+
|
|
114
|
+
@@contexts[ctx.to_key] ||= ctx
|
|
115
|
+
@@contexts[ctx.to_key].source_transform << block
|
|
116
|
+
|
|
117
|
+
@@default_context = @@contexts.values.first if @@contexts.size == 1
|
|
87
118
|
end
|
|
88
119
|
|
|
89
120
|
# This hook should be used to manually compile byte code to be loaded by the VM.
|
|
@@ -101,32 +132,35 @@ module RequireHooks
|
|
|
101
132
|
# end
|
|
102
133
|
# end
|
|
103
134
|
def hijack_load(patterns: nil, exclude_patterns: nil, &block)
|
|
104
|
-
@@
|
|
135
|
+
@@default_context = nil
|
|
136
|
+
ctx = Context.new(patterns: patterns, exclude_patterns: exclude_patterns)
|
|
137
|
+
|
|
138
|
+
@@contexts[ctx.to_key] ||= ctx
|
|
139
|
+
@@contexts[ctx.to_key].hijack_load << block
|
|
140
|
+
|
|
141
|
+
@@default_context = @@contexts.values.first if @@contexts.size == 1
|
|
105
142
|
end
|
|
106
143
|
|
|
107
144
|
def context_for(path)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
true
|
|
113
|
-
end.map { |_patterns, _exclude_patterns, block| block }
|
|
145
|
+
# Fast-track in case we have just a single context defined
|
|
146
|
+
if @@default_context
|
|
147
|
+
return @@noop_context unless @@default_context.match?(path)
|
|
114
148
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
next if exclude_patterns&.any? { |pattern| File.fnmatch?(pattern, path) }
|
|
149
|
+
return @@default_context
|
|
150
|
+
end
|
|
118
151
|
|
|
119
|
-
|
|
120
|
-
end.map { |_patterns, _exclude_patterns, block| block }
|
|
152
|
+
matching = @@contexts.values.select { |ctx| ctx.match?(path) }
|
|
121
153
|
|
|
122
|
-
|
|
123
|
-
next unless !patterns || patterns.any? { |pattern| File.fnmatch?(pattern, path) }
|
|
124
|
-
next if exclude_patterns&.any? { |pattern| File.fnmatch?(pattern, path) }
|
|
154
|
+
return matching[0] || @@noop_context if matching.size < 2
|
|
125
155
|
|
|
126
|
-
|
|
127
|
-
|
|
156
|
+
ctx = Context.new
|
|
157
|
+
matching.each do |mctx|
|
|
158
|
+
ctx.around_load.concat(mctx.around_load)
|
|
159
|
+
ctx.source_transform.concat(mctx.source_transform)
|
|
160
|
+
ctx.hijack_load.concat(mctx.hijack_load)
|
|
161
|
+
end
|
|
128
162
|
|
|
129
|
-
|
|
163
|
+
ctx
|
|
130
164
|
end
|
|
131
165
|
end
|
|
132
166
|
end
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module RequireHooks
|
|
4
4
|
module Bootsnap
|
|
5
|
+
EMPTY_ISEQ = RubyVM::InstructionSequence.compile("").freeze
|
|
6
|
+
|
|
5
7
|
module CompileCacheExt
|
|
6
8
|
def input_to_storage(source, path, *)
|
|
7
9
|
ctx = RequireHooks.context_for(path)
|
|
10
|
+
return super if ctx.empty?
|
|
8
11
|
|
|
9
12
|
new_contents = ctx.perform_source_transform(path)
|
|
10
13
|
hijacked = ctx.try_hijack_load(path, new_contents)
|
|
@@ -26,7 +29,17 @@ module RequireHooks
|
|
|
26
29
|
# Around hooks must be performed every time we trigger a file load, even if
|
|
27
30
|
# the file is already cached.
|
|
28
31
|
def load_iseq(path)
|
|
29
|
-
RequireHooks.context_for(path)
|
|
32
|
+
ctx = RequireHooks.context_for(path)
|
|
33
|
+
# Early-return for non-trackable paths
|
|
34
|
+
return super if ctx.empty?
|
|
35
|
+
|
|
36
|
+
ctx.run_around_load_callbacks(path) do
|
|
37
|
+
iseq = super
|
|
38
|
+
return unless iseq
|
|
39
|
+
|
|
40
|
+
iseq.eval
|
|
41
|
+
EMPTY_ISEQ
|
|
42
|
+
end
|
|
30
43
|
end
|
|
31
44
|
end
|
|
32
45
|
end
|
|
@@ -5,8 +5,8 @@ require "pathname"
|
|
|
5
5
|
module RequireHooks
|
|
6
6
|
module KernelPatch
|
|
7
7
|
class << self
|
|
8
|
-
def load(path)
|
|
9
|
-
ctx
|
|
8
|
+
def load(path, ctx: nil)
|
|
9
|
+
ctx ||= RequireHooks.context_for(path)
|
|
10
10
|
|
|
11
11
|
ctx.run_around_load_callbacks(path) do
|
|
12
12
|
next load_without_require_hooks(path) unless ctx.source_transform? || ctx.hijack?
|
|
@@ -140,32 +140,6 @@ module RequireHooks
|
|
|
140
140
|
path
|
|
141
141
|
end
|
|
142
142
|
|
|
143
|
-
# Based on https://github.com/ruby/ruby/blob/b588fd552390c55809719100d803c36bc7430f2f/load.c#L403-L415
|
|
144
|
-
def feature_loaded?(feature)
|
|
145
|
-
return true if $LOADED_FEATURES.include?(feature) && !LOCK.locked_feature?(feature)
|
|
146
|
-
|
|
147
|
-
feature = Pathname.new(feature).cleanpath.to_s
|
|
148
|
-
efeature = File.expand_path(feature)
|
|
149
|
-
|
|
150
|
-
# Check absoulute and relative paths
|
|
151
|
-
return true if $LOADED_FEATURES.include?(efeature) && !LOCK.locked_feature?(efeature)
|
|
152
|
-
|
|
153
|
-
candidates = []
|
|
154
|
-
|
|
155
|
-
$LOADED_FEATURES.each do |lf|
|
|
156
|
-
candidates << lf if lf.end_with?("/#{feature}")
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
return false if candidates.empty?
|
|
160
|
-
|
|
161
|
-
$LOAD_PATH.each do |lp|
|
|
162
|
-
lp_feature = File.join(lp, feature)
|
|
163
|
-
return true if candidates.include?(lp_feature) && !LOCK.locked_feature?(lp_feature)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
false
|
|
167
|
-
end
|
|
168
|
-
|
|
169
143
|
private
|
|
170
144
|
|
|
171
145
|
def lookup_feature_path(path, implitic_ext: true)
|
|
@@ -250,13 +224,13 @@ module Kernel
|
|
|
250
224
|
|
|
251
225
|
return require_without_require_hooks(path) if ctx.empty?
|
|
252
226
|
|
|
253
|
-
return false if
|
|
227
|
+
return false if $LOADED_FEATURES.include?(realpath)
|
|
254
228
|
|
|
255
229
|
RequireHooks::KernelPatch::Features::LOCK.lock_feature(feature) do |loaded|
|
|
256
230
|
return false if loaded
|
|
257
231
|
|
|
258
232
|
$LOADED_FEATURES << realpath
|
|
259
|
-
RequireHooks::KernelPatch.load(realpath)
|
|
233
|
+
RequireHooks::KernelPatch.load(realpath, ctx: ctx)
|
|
260
234
|
true
|
|
261
235
|
end
|
|
262
236
|
rescue LoadError => e
|
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RequireHooks
|
|
4
|
+
EMPTY_ISEQ = RubyVM::InstructionSequence.compile("").freeze
|
|
5
|
+
|
|
4
6
|
module LoadIseq
|
|
5
7
|
def load_iseq(path)
|
|
6
8
|
ctx = RequireHooks.context_for(path)
|
|
7
9
|
|
|
10
|
+
# Early-return for non-trackable paths
|
|
11
|
+
return if ctx.empty?
|
|
12
|
+
|
|
8
13
|
ctx.run_around_load_callbacks(path) do
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
iseq =
|
|
15
|
+
if ctx.source_transform? || ctx.hijack?
|
|
16
|
+
new_contents = ctx.perform_source_transform(path)
|
|
17
|
+
hijacked = ctx.try_hijack_load(path, new_contents)
|
|
18
|
+
|
|
19
|
+
if hijacked
|
|
20
|
+
raise TypeError, "Unsupported bytecode format for #{path}: #{hijack.class}" unless hijacked.is_a?(::RubyVM::InstructionSequence)
|
|
21
|
+
hijacked
|
|
22
|
+
elsif new_contents
|
|
23
|
+
RubyVM::InstructionSequence.compile(new_contents, path, path, 1)
|
|
24
|
+
end
|
|
18
25
|
end
|
|
19
|
-
end
|
|
20
26
|
|
|
21
|
-
defined?(super) ? super : RubyVM::InstructionSequence.compile_file(path)
|
|
27
|
+
iseq ||= (defined?(super) ? super : RubyVM::InstructionSequence.compile_file(path))
|
|
28
|
+
iseq.eval
|
|
29
|
+
EMPTY_ISEQ
|
|
22
30
|
end
|
|
23
31
|
end
|
|
24
32
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: require-hooks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vladimir Dementyev
|
|
@@ -37,6 +37,7 @@ metadata:
|
|
|
37
37
|
homepage_uri: https://github.com/ruby-next/require-hooks
|
|
38
38
|
source_code_uri: https://github.com/ruby-next/require-hooks
|
|
39
39
|
funding_uri: https://github.com/sponsors/palkan
|
|
40
|
+
rubygems_mfa_required: 'true'
|
|
40
41
|
rdoc_options: []
|
|
41
42
|
require_paths:
|
|
42
43
|
- lib
|