rails-dev-boost 0.1.1 → 0.2.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.
- data/README.markdown +240 -39
- data/VERSION +1 -1
- data/lib/rails-dev-boost.rb +1 -0
- data/lib/rails_development_boost.rb +44 -15
- data/lib/rails_development_boost/async.rb +73 -0
- data/lib/rails_development_boost/dependencies_patch.rb +245 -123
- data/lib/rails_development_boost/dependencies_patch/instrumentation_patch.rb +171 -0
- data/lib/rails_development_boost/descendants_tracker_patch.rb +2 -0
- data/lib/rails_development_boost/loadable_patch.rb +18 -0
- data/lib/rails_development_boost/loaded_file.rb +207 -30
- data/lib/rails_development_boost/observable_patch.rb +38 -0
- data/lib/rails_development_boost/reference_cleanup_patch.rb +14 -0
- data/lib/rails_development_boost/reference_patch.rb +4 -0
- data/lib/rails_development_boost/reloader.rb +51 -0
- data/lib/rails_development_boost/required_dependency.rb +36 -0
- data/lib/rails_development_boost/view_helpers_patch.rb +5 -1
- metadata +28 -26
- data/.gitignore +0 -1
- data/Rakefile +0 -52
- data/TODO.txt +0 -2
- data/init.rb +0 -3
- data/rails-dev-boost.gemspec +0 -116
@@ -0,0 +1,171 @@
|
|
1
|
+
module RailsDevelopmentBoost
|
2
|
+
module DependenciesPatch
|
3
|
+
module InstrumentationPatch
|
4
|
+
module Instrumenter
|
5
|
+
delegate :boost_log, :to => 'ActiveSupport::Dependencies'
|
6
|
+
|
7
|
+
def self.included(mod)
|
8
|
+
mod.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def included(klass)
|
13
|
+
(public_instance_methods(false) + private_instance_methods(false) + protected_instance_methods(false)).each do |method|
|
14
|
+
if m = method.to_s.match(/\A(.+)_with_(.+)\Z/)
|
15
|
+
meth_name, extension = m[1], m[2]
|
16
|
+
extension.sub!(/[?!=]\Z/) do |modifier|
|
17
|
+
meth_name << modifier
|
18
|
+
''
|
19
|
+
end
|
20
|
+
klass.alias_method_chain meth_name, extension
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module LoadedFile
|
30
|
+
include Instrumenter
|
31
|
+
|
32
|
+
def boost_inspect
|
33
|
+
"\#<LoadedFile #{relative_path} #{inspect_constants(@constants)}>"
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_constants_with_instrumentation(new_constants)
|
37
|
+
boost_log('ADD_CONSTANTS', "#{boost_inspect} <- #{inspect_constants(new_constants)}")
|
38
|
+
add_constants_without_instrumentation(new_constants)
|
39
|
+
end
|
40
|
+
|
41
|
+
def unload_dependent_file_with_instrumentation(dependent_file)
|
42
|
+
boost_log('UNLOAD_DEPENDENT', "#{boost_inspect}: #{dependent_file.boost_inspect}")
|
43
|
+
unload_dependent_file_without_instrumentation(dependent_file)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
RAILS_ROOT = /\A#{Rails.root.to_s}/
|
48
|
+
|
49
|
+
def inspect_constants(constants_arr)
|
50
|
+
"[#{constants_arr.join(', ')}]"
|
51
|
+
end
|
52
|
+
|
53
|
+
def relative_path
|
54
|
+
@path.sub(RAILS_ROOT, '')
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.included(klass)
|
58
|
+
klass.singleton_class.send :include, ClassMethods
|
59
|
+
super
|
60
|
+
end
|
61
|
+
|
62
|
+
module ClassMethods
|
63
|
+
include Instrumenter
|
64
|
+
|
65
|
+
def unload_modified_with_instrumentation!
|
66
|
+
boost_log('--- START ---')
|
67
|
+
unload_modified_without_instrumentation!.tap do
|
68
|
+
boost_log('--- END ---')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def unload_containing_file_with_instrumentation(const_name, file)
|
73
|
+
boost_log('UNLOAD_CONTAINING_FILE', "#{const_name} -> #{file.boost_inspect}")
|
74
|
+
unload_containing_file_without_instrumentation(const_name, file)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
module Files
|
80
|
+
include Instrumenter
|
81
|
+
|
82
|
+
def unload_modified_file_with_instrumentation(file)
|
83
|
+
boost_log('CHANGED', "#{file.boost_inspect}")
|
84
|
+
unload_modified_file_without_instrumentation(file)
|
85
|
+
end
|
86
|
+
|
87
|
+
def unload_decorator_file_with_instrumentation(file)
|
88
|
+
boost_log('UNLOAD_DECORATOR_FILE', "#{file.boost_inspect}")
|
89
|
+
unload_decorator_file_without_instrumentation(file)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.apply!
|
94
|
+
unless applied?
|
95
|
+
ActiveSupport::Dependencies.extend self
|
96
|
+
RailsDevelopmentBoost::LoadedFile.send :include, LoadedFile
|
97
|
+
RailsDevelopmentBoost::LoadedFile::Files.send :include, Files
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.applied?
|
102
|
+
ActiveSupport::Dependencies.singleton_class.include?(self)
|
103
|
+
end
|
104
|
+
|
105
|
+
def load_file_without_constant_tracking(path, *args)
|
106
|
+
other_args = ", #{args.map(&:inspect).join(', ')}" if args.any?
|
107
|
+
boost_log('LOAD', "load_file(#{path.inspect}#{other_args})")
|
108
|
+
super
|
109
|
+
end
|
110
|
+
|
111
|
+
def remove_constant_without_handling_of_connections(const_name)
|
112
|
+
boost_log('REMOVE_CONST', const_name)
|
113
|
+
super
|
114
|
+
end
|
115
|
+
|
116
|
+
def load_file_from_explicit_load(expanded_path)
|
117
|
+
boost_log('EXPLICIT_LOAD_REQUEST', expanded_path)
|
118
|
+
super
|
119
|
+
end
|
120
|
+
|
121
|
+
def boost_log(action, msg = nil)
|
122
|
+
action, msg = msg, action unless msg
|
123
|
+
raw_boost_log("#{ "[#{action}] " if action}#{msg}")
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
def unprotected_remove_constant(const_name)
|
128
|
+
boost_log('REMOVING', const_name)
|
129
|
+
@removal_nesting = (@removal_nesting || 0) + 1
|
130
|
+
super
|
131
|
+
ensure
|
132
|
+
@removal_nesting -= 1
|
133
|
+
end
|
134
|
+
|
135
|
+
def error_loading_file(file_path, e)
|
136
|
+
description = RailsDevelopmentBoost::LoadedFile.loaded?(file_path) ? RailsDevelopmentBoost::LoadedFile.for(file_path).boost_inspect : file_path
|
137
|
+
boost_log('ERROR_WHILE_LOADING', "#{description}: #{e.inspect}")
|
138
|
+
super
|
139
|
+
end
|
140
|
+
|
141
|
+
def remove_explicit_dependency(const_name, depending_const)
|
142
|
+
boost_log('EXPLICIT_DEPENDENCY', "#{const_name} -> #{depending_const}")
|
143
|
+
super
|
144
|
+
end
|
145
|
+
|
146
|
+
def remove_dependent_constant(original_module, dependent_module)
|
147
|
+
boost_log('DEPENDENT_MODULE', "#{original_module._mod_name} -> #{dependent_module._mod_name}")
|
148
|
+
super
|
149
|
+
end
|
150
|
+
|
151
|
+
def remove_autoloaded_parent_module(initial_object, parent_object)
|
152
|
+
boost_log('REMOVE_PARENT', "#{initial_object._mod_name} -> #{parent_object._mod_name}")
|
153
|
+
super
|
154
|
+
end
|
155
|
+
|
156
|
+
def remove_child_module_constant(parent_object, full_child_const_name)
|
157
|
+
boost_log('REMOVE_CHILD', "#{parent_object._mod_name} -> #{full_child_const_name}")
|
158
|
+
super
|
159
|
+
end
|
160
|
+
|
161
|
+
def remove_nested_constant(parent_const, child_const)
|
162
|
+
boost_log('REMOVE_NESTED', "#{parent_const} :: #{child_const.sub(/\A#{parent_const}::/, '')}")
|
163
|
+
super
|
164
|
+
end
|
165
|
+
|
166
|
+
def raw_boost_log(msg)
|
167
|
+
Rails.logger.info("[DEV-BOOST] #{"\t" * (@removal_nesting || 0)}#{msg}")
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module RailsDevelopmentBoost
|
2
|
+
module LoadablePatch
|
3
|
+
def self.apply!
|
4
|
+
Object.send :include, LoadablePatch
|
5
|
+
end
|
6
|
+
|
7
|
+
def load(file, wrap = false)
|
8
|
+
expanded_path = File.expand_path(file)
|
9
|
+
# force the manual #load calls for autoloadable files to go through the AS::Dep stack
|
10
|
+
if ActiveSupport::Dependencies.in_autoload_path?(expanded_path)
|
11
|
+
expanded_path << '.rb' unless expanded_path =~ /\.(rb|rake)\Z/
|
12
|
+
ActiveSupport::Dependencies.load_file_from_explicit_load(expanded_path)
|
13
|
+
else
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,13 +1,98 @@
|
|
1
1
|
module RailsDevelopmentBoost
|
2
2
|
class LoadedFile
|
3
|
-
|
3
|
+
class Files < Hash
|
4
|
+
def initialize(*args)
|
5
|
+
super {|hash, file_path| hash[file_path] = LoadedFile.new(file_path)}
|
6
|
+
end
|
7
|
+
|
8
|
+
def unload_modified!
|
9
|
+
each_file_unload_if_changed {|file| file.changed?}
|
10
|
+
end
|
11
|
+
|
12
|
+
def each_file_unload_if_changed
|
13
|
+
unloaded_something = false
|
14
|
+
values.each do |file|
|
15
|
+
if yield(file)
|
16
|
+
unload_modified_file(file)
|
17
|
+
unloaded_something = true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
if unloaded_something
|
21
|
+
values.each do |file|
|
22
|
+
unload_decorator_file(file) if file.decorator_like?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
unloaded_something
|
26
|
+
end
|
27
|
+
|
28
|
+
def unload_modified_file(file)
|
29
|
+
file.unload!
|
30
|
+
end
|
31
|
+
|
32
|
+
def unload_decorator_file(file)
|
33
|
+
file.unload!
|
34
|
+
end
|
35
|
+
|
36
|
+
def constants
|
37
|
+
values.map(&:constants).flatten
|
38
|
+
end
|
39
|
+
|
40
|
+
def stored?(file)
|
41
|
+
key?(file.path) && self[file.path] == file
|
42
|
+
end
|
43
|
+
|
44
|
+
alias_method :loaded?, :key?
|
45
|
+
end
|
4
46
|
|
5
|
-
class
|
6
|
-
|
47
|
+
class ConstantsToFiles < Hash
|
48
|
+
def associate(const_name, file)
|
49
|
+
(self[const_name] ||= []) << file
|
50
|
+
end
|
51
|
+
|
52
|
+
def deassociate(const_name, file)
|
53
|
+
if files = self[const_name]
|
54
|
+
files.delete(file)
|
55
|
+
delete(const_name) if files.empty?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def each_file_with_const(const_name, &block)
|
60
|
+
if files = self[const_name]
|
61
|
+
files.dup.each(&block)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Interdependencies < Hash
|
67
|
+
def associate(file_a, file_b)
|
68
|
+
(self[file_a] ||= Set.new) << file_b
|
69
|
+
(self[file_b] ||= Set.new) << file_a
|
70
|
+
end
|
71
|
+
|
72
|
+
def each_dependent_on(file)
|
73
|
+
if deps = delete(file)
|
74
|
+
deps.each do |dep|
|
75
|
+
deassociate(dep, file)
|
76
|
+
yield dep
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
def deassociate(file_a, file_b)
|
83
|
+
if deps = self[file_a]
|
84
|
+
deps.delete(file_b)
|
85
|
+
delete(file_a) if deps.empty?
|
86
|
+
end
|
87
|
+
end
|
7
88
|
end
|
8
89
|
|
90
|
+
LOADED = Files.new
|
91
|
+
CONSTANTS_TO_FILES = ConstantsToFiles.new
|
92
|
+
INTERDEPENDENCIES = Interdependencies.new
|
93
|
+
NOW_UNLOADING = Set.new
|
94
|
+
|
9
95
|
attr_accessor :path, :constants
|
10
|
-
delegate :constants_to_files, :to => 'self.class'
|
11
96
|
|
12
97
|
def initialize(path, constants=[])
|
13
98
|
@path = path
|
@@ -21,53 +106,145 @@ module RailsDevelopmentBoost
|
|
21
106
|
end
|
22
107
|
|
23
108
|
def add_constants(new_constants)
|
24
|
-
new_constants.each
|
25
|
-
(constants_to_files[new_constant] ||= []) << self
|
26
|
-
end
|
109
|
+
new_constants.each {|new_constant| CONSTANTS_TO_FILES.associate(new_constant, self)}
|
27
110
|
@constants |= new_constants
|
28
|
-
retrieve_associated_files.each {|file| file.add_constants(@constants)} if @associated_files
|
29
111
|
end
|
30
112
|
|
31
|
-
|
32
|
-
|
33
|
-
|
113
|
+
# "decorator" files are popular with certain Rails frameworks (spree/refinerycms etc.) they don't define their own constants, instead
|
114
|
+
# they are usually used for adding methods to other classes via Model.class_eval { def meth; end }
|
115
|
+
def decorator_like?
|
116
|
+
@constants.empty? && !INTERDEPENDENCIES[self]
|
34
117
|
end
|
35
118
|
|
36
|
-
def
|
37
|
-
|
119
|
+
def unload!
|
120
|
+
guard_double_unloading do
|
121
|
+
INTERDEPENDENCIES.each_dependent_on(self) {|dependent_file| unload_dependent_file(dependent_file)}
|
122
|
+
@constants.dup.each {|const| ActiveSupport::Dependencies.remove_constant(const)}
|
123
|
+
clean_up_if_necessary
|
124
|
+
end
|
38
125
|
end
|
39
126
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
127
|
+
def associate_to_greppable_constants # brute-force approach
|
128
|
+
# we don't know anything about the constants contained in the files up the currently_loading stack
|
129
|
+
ActiveSupport::Dependencies.currently_loading.each {|path| self.class.relate_files(path, self)}
|
130
|
+
add_constants(greppable_constants)
|
43
131
|
end
|
44
132
|
|
45
|
-
|
46
|
-
|
133
|
+
# It is important to catch all the intermediate constants as they might be "nested" constants, that are generally not tracked by AS::Dependencies.
|
134
|
+
# Pathological example is as follows:
|
135
|
+
#
|
136
|
+
# File `a.rb` contains `class A; X = :x; end`. AS::Dependencies only associates the 'A' const to the `a.rb` file (ignoring the nested 'A::X'), while
|
137
|
+
# a decorator file `b_decorator.rb` containing `B.class_eval {A::X}` would grep and find the `A::X` const, check that indeed it is in autoloaded
|
138
|
+
# namespace and associate it to itself. When `b_decorator.rb` is then being unloaded it simply does `remove_constant('A::X')` while failing to trigger
|
139
|
+
# the unloading of `a.rb`.
|
140
|
+
def greppable_constants
|
141
|
+
constants = []
|
142
|
+
read_greppable_constants.each do |const_name|
|
143
|
+
intermediates = nil
|
144
|
+
begin
|
145
|
+
if self.class.loaded_constant?(const_name)
|
146
|
+
constants << const_name
|
147
|
+
constants.concat(intermediates) if intermediates
|
148
|
+
break
|
149
|
+
end
|
150
|
+
(intermediates ||= []) << const_name.dup
|
151
|
+
end while const_name.sub!(/::[^:]+\Z/, '')
|
152
|
+
end
|
153
|
+
constants.uniq
|
154
|
+
end
|
155
|
+
|
156
|
+
def read_greppable_constants
|
157
|
+
File.read(@path).scan(/[A-Z][_A-Za-z0-9]*(?:::[A-Z][_A-Za-z0-9]*)*/).uniq
|
47
158
|
end
|
48
159
|
|
49
|
-
|
50
|
-
|
51
|
-
|
160
|
+
# consistent hashing
|
161
|
+
def hash
|
162
|
+
@path.hash
|
163
|
+
end
|
164
|
+
|
165
|
+
def eql?(other)
|
166
|
+
@path.eql?(other)
|
167
|
+
end
|
168
|
+
|
169
|
+
def unload_dependent_file(dependent_file)
|
170
|
+
dependent_file.unload!
|
171
|
+
end
|
172
|
+
|
173
|
+
def guard_double_unloading
|
174
|
+
if NOW_UNLOADING.add?(self)
|
175
|
+
begin
|
176
|
+
yield
|
177
|
+
ensure
|
178
|
+
NOW_UNLOADING.delete(self)
|
179
|
+
end
|
52
180
|
end
|
53
181
|
end
|
54
182
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
|
183
|
+
def delete_constant(const_name)
|
184
|
+
CONSTANTS_TO_FILES.deassociate(const_name, self)
|
185
|
+
@constants.delete(const_name)
|
186
|
+
clean_up_if_necessary
|
187
|
+
end
|
188
|
+
|
189
|
+
def clean_up_if_necessary
|
190
|
+
if @constants.empty? && LOADED.stored?(self)
|
191
|
+
LOADED.delete(@path)
|
192
|
+
ActiveSupport::Dependencies.loaded.delete(require_path)
|
59
193
|
end
|
60
194
|
end
|
61
195
|
|
62
|
-
|
196
|
+
def associate_with(other_loaded_file)
|
197
|
+
INTERDEPENDENCIES.associate(self, other_loaded_file)
|
198
|
+
end
|
199
|
+
|
200
|
+
def require_path
|
201
|
+
File.expand_path(@path.sub(/\.rb\Z/, '')) # be sure to do the same thing as Dependencies#require_or_load and use the expanded path
|
202
|
+
end
|
63
203
|
|
64
|
-
def
|
65
|
-
|
66
|
-
|
67
|
-
|
204
|
+
def stale!
|
205
|
+
@mtime = 0
|
206
|
+
INTERDEPENDENCIES.each_dependent_on(self, &:stale!)
|
207
|
+
end
|
208
|
+
|
209
|
+
class << self
|
210
|
+
def unload_modified!
|
211
|
+
LOADED.unload_modified!
|
212
|
+
end
|
213
|
+
|
214
|
+
def for(file_path)
|
215
|
+
LOADED[file_path]
|
216
|
+
end
|
217
|
+
|
218
|
+
def loaded?(file_path)
|
219
|
+
LOADED.loaded?(file_path)
|
220
|
+
end
|
221
|
+
|
222
|
+
def loaded_constants
|
223
|
+
LOADED.constants
|
224
|
+
end
|
225
|
+
|
226
|
+
def loaded_constant?(const_name)
|
227
|
+
CONSTANTS_TO_FILES[const_name]
|
228
|
+
end
|
229
|
+
|
230
|
+
def unload_files_with_const!(const_name)
|
231
|
+
CONSTANTS_TO_FILES.each_file_with_const(const_name) {|file| unload_containing_file(const_name, file)}
|
232
|
+
end
|
233
|
+
|
234
|
+
def unload_containing_file(const_name, file)
|
235
|
+
file.unload!
|
236
|
+
end
|
237
|
+
|
238
|
+
def const_unloaded(const_name)
|
239
|
+
CONSTANTS_TO_FILES.each_file_with_const(const_name) {|file| file.delete_constant(const_name)}
|
240
|
+
end
|
241
|
+
|
242
|
+
def relate_files(base_file, related_file)
|
243
|
+
LOADED[base_file].associate_with(LOADED[related_file])
|
68
244
|
end
|
69
245
|
end
|
70
246
|
|
247
|
+
private
|
71
248
|
def current_mtime
|
72
249
|
# trying to be more efficient: there is no need for a full-fledged Time instance, just grab the timestamp
|
73
250
|
File.mtime(@path).to_i rescue nil
|