require-hooks 0.2.2 → 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 +12 -0
- data/README.md +1 -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 -31
- data/lib/require-hooks/mode/load_iseq.rb +19 -11
- data/lib/require-hooks/version.rb +1 -1
- metadata +10 -12
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,18 @@
|
|
|
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
|
+
|
|
13
|
+
## 0.2.3 (2026-01-13)
|
|
14
|
+
|
|
15
|
+
- Gem metadata fixes.
|
|
16
|
+
|
|
5
17
|
## 0.2.2 (2023-12-19)
|
|
6
18
|
|
|
7
19
|
- Fix handing uncompilable source code with Bootsnap. ([@palkan][])
|
data/README.md
CHANGED
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
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "mutex_m"
|
|
4
3
|
require "pathname"
|
|
5
4
|
|
|
6
5
|
module RequireHooks
|
|
7
6
|
module KernelPatch
|
|
8
7
|
class << self
|
|
9
|
-
def load(path)
|
|
10
|
-
ctx
|
|
8
|
+
def load(path, ctx: nil)
|
|
9
|
+
ctx ||= RequireHooks.context_for(path)
|
|
11
10
|
|
|
12
11
|
ctx.run_around_load_callbacks(path) do
|
|
13
12
|
next load_without_require_hooks(path) unless ctx.source_transform? || ctx.hijack?
|
|
@@ -141,32 +140,6 @@ module RequireHooks
|
|
|
141
140
|
path
|
|
142
141
|
end
|
|
143
142
|
|
|
144
|
-
# Based on https://github.com/ruby/ruby/blob/b588fd552390c55809719100d803c36bc7430f2f/load.c#L403-L415
|
|
145
|
-
def feature_loaded?(feature)
|
|
146
|
-
return true if $LOADED_FEATURES.include?(feature) && !LOCK.locked_feature?(feature)
|
|
147
|
-
|
|
148
|
-
feature = Pathname.new(feature).cleanpath.to_s
|
|
149
|
-
efeature = File.expand_path(feature)
|
|
150
|
-
|
|
151
|
-
# Check absoulute and relative paths
|
|
152
|
-
return true if $LOADED_FEATURES.include?(efeature) && !LOCK.locked_feature?(efeature)
|
|
153
|
-
|
|
154
|
-
candidates = []
|
|
155
|
-
|
|
156
|
-
$LOADED_FEATURES.each do |lf|
|
|
157
|
-
candidates << lf if lf.end_with?("/#{feature}")
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
return false if candidates.empty?
|
|
161
|
-
|
|
162
|
-
$LOAD_PATH.each do |lp|
|
|
163
|
-
lp_feature = File.join(lp, feature)
|
|
164
|
-
return true if candidates.include?(lp_feature) && !LOCK.locked_feature?(lp_feature)
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
false
|
|
168
|
-
end
|
|
169
|
-
|
|
170
143
|
private
|
|
171
144
|
|
|
172
145
|
def lookup_feature_path(path, implitic_ext: true)
|
|
@@ -251,13 +224,13 @@ module Kernel
|
|
|
251
224
|
|
|
252
225
|
return require_without_require_hooks(path) if ctx.empty?
|
|
253
226
|
|
|
254
|
-
return false if
|
|
227
|
+
return false if $LOADED_FEATURES.include?(realpath)
|
|
255
228
|
|
|
256
229
|
RequireHooks::KernelPatch::Features::LOCK.lock_feature(feature) do |loaded|
|
|
257
230
|
return false if loaded
|
|
258
231
|
|
|
259
232
|
$LOADED_FEATURES << realpath
|
|
260
|
-
RequireHooks::KernelPatch.load(realpath)
|
|
233
|
+
RequireHooks::KernelPatch.load(realpath, ctx: ctx)
|
|
261
234
|
true
|
|
262
235
|
end
|
|
263
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,14 +1,13 @@
|
|
|
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
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
12
|
description: Require Hooks provide infrastructure for intercepting require/load calls
|
|
14
13
|
in Ruby
|
|
@@ -28,17 +27,17 @@ files:
|
|
|
28
27
|
- lib/require-hooks/mode/load_iseq.rb
|
|
29
28
|
- lib/require-hooks/setup.rb
|
|
30
29
|
- lib/require-hooks/version.rb
|
|
31
|
-
homepage: https://github.com/ruby-next/
|
|
30
|
+
homepage: https://github.com/ruby-next/require-hooks
|
|
32
31
|
licenses:
|
|
33
32
|
- MIT
|
|
34
33
|
metadata:
|
|
35
|
-
bug_tracker_uri: https://github.com/ruby-next/
|
|
36
|
-
changelog_uri: https://github.com/ruby-next/
|
|
37
|
-
documentation_uri: https://github.com/ruby-next/
|
|
38
|
-
homepage_uri: https://github.com/ruby-next/
|
|
39
|
-
source_code_uri: https://github.com/ruby-next/
|
|
34
|
+
bug_tracker_uri: https://github.com/ruby-next/require-hooks/issues
|
|
35
|
+
changelog_uri: https://github.com/ruby-next/require-hooks/blob/master/CHANGELOG.md
|
|
36
|
+
documentation_uri: https://github.com/ruby-next/require-hooks/blob/master/README.md
|
|
37
|
+
homepage_uri: https://github.com/ruby-next/require-hooks
|
|
38
|
+
source_code_uri: https://github.com/ruby-next/require-hooks
|
|
40
39
|
funding_uri: https://github.com/sponsors/palkan
|
|
41
|
-
|
|
40
|
+
rubygems_mfa_required: 'true'
|
|
42
41
|
rdoc_options: []
|
|
43
42
|
require_paths:
|
|
44
43
|
- lib
|
|
@@ -53,8 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
53
|
version: '0'
|
|
55
54
|
requirements: []
|
|
56
|
-
rubygems_version: 3.
|
|
57
|
-
signing_key:
|
|
55
|
+
rubygems_version: 3.6.9
|
|
58
56
|
specification_version: 4
|
|
59
57
|
summary: Require Hooks provide infrastructure for intercepting require/load calls
|
|
60
58
|
in Ruby.
|