actionview_precompiler 0.2.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ module ActionviewPrecompiler
2
+ class HelperParser
3
+ def initialize(filename)
4
+ @filename = filename
5
+ end
6
+
7
+ def render_calls
8
+ src = File.read(@filename)
9
+ return [] unless src.include?("render")
10
+ RenderParser.new(src).render_calls
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,34 @@
1
+ module ActionviewPrecompiler
2
+ class HelperScanner
3
+ def initialize(dir)
4
+ @dir = dir
5
+ end
6
+
7
+ def template_renders
8
+ template_renders = []
9
+
10
+ each_helper do |fullpath|
11
+ parser = HelperParser.new(fullpath)
12
+ parser.render_calls.each do |render_call|
13
+ virtual_path = render_call.virtual_path
14
+
15
+ locals = render_call.locals_keys.map(&:to_s).sort
16
+
17
+ template_renders << [virtual_path, locals]
18
+ end
19
+ end
20
+
21
+ template_renders.uniq
22
+ end
23
+
24
+ private
25
+
26
+ def each_helper
27
+ Dir["#{@dir}/**/*_helper.rb"].sort.map do |fullpath|
28
+ next if File.directory?(fullpath)
29
+
30
+ yield fullpath
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,95 +1,69 @@
1
+ require "actionview_precompiler/template_scanner"
2
+ require "actionview_precompiler/controller_scanner"
3
+ require "actionview_precompiler/helper_scanner"
4
+ require "actionview_precompiler/template_loader"
5
+
1
6
  module ActionviewPrecompiler
2
7
  class Precompiler
3
- class Template
4
- attr_reader :fullpath, :relative_path, :virtual_path
5
- attr_reader :action, :prefix, :details
6
-
7
- def initialize(fullpath, relative_path)
8
- @fullpath = fullpath
9
- @relative_path = relative_path
10
- @virtual_path = relative_path.slice(0, relative_path.index("."))
11
-
12
- parsed = ParsedFilename.new(relative_path)
13
- @prefix = parsed.prefix
14
- @action = parsed.action
15
- @partial = parsed.partial?
16
- @details = parsed.details
17
- end
18
-
19
- def partial?
20
- @partial
21
- end
8
+ def initialize(verbose: false)
9
+ @scanners = []
10
+ @loader = TemplateLoader.new
11
+ @verbose = verbose
12
+ @static_templates = []
13
+ @template_renders = nil
22
14
  end
23
15
 
24
- attr_reader :templates
16
+ def scan_view_dir(view_dir)
17
+ @scanners << TemplateScanner.new(view_dir)
18
+ end
25
19
 
26
- def initialize(view_dirs)
27
- @templates =
28
- view_dirs.flat_map do |view_dir|
29
- Dir["**/*", base: view_dir].map do |file|
30
- fullpath = File.expand_path(file, view_dir)
31
- next if File.directory?(fullpath)
20
+ def scan_controller_dir(controller_dir)
21
+ @scanners << ControllerScanner.new(controller_dir)
22
+ end
32
23
 
33
- Template.new(fullpath, file)
34
- end.compact
35
- end
24
+ def scan_helper_dir(controller_dir)
25
+ @scanners << HelperScanner.new(controller_dir)
26
+ end
36
27
 
37
- determine_locals
28
+ def add_template(virtual_path, locals = [])
29
+ locals = locals.map(&:to_s).sort
30
+ @static_templates << [virtual_path, locals]
38
31
  end
39
32
 
40
- def each_lookup_args
41
- return enum_for(__method__) unless block_given?
33
+ def run
34
+ count = 0
35
+ template_renders.each do |virtual_path, locals|
36
+ debug "precompiling: #{virtual_path}"
42
37
 
43
- each_template_render do |template, locals|
44
- details = {
45
- locale: Array(template.details[:locale]),
46
- variants: Array(template.details[:variant]),
47
- formats: Array(template.details[:format]),
48
- handlers: Array(template.details[:handler])
49
- }
38
+ templates = @loader.load_template(virtual_path, locals)
39
+ count += templates.count
50
40
 
51
- yield [template.action, template.prefix, template.partial?, locals, details]
41
+ debug " No templates found at #{virtual_path}" if templates.empty?
52
42
  end
53
43
 
54
- nil
44
+ debug "Precompiled #{count} Templates"
55
45
  end
56
46
 
57
- def each_template_render
58
- return enum_for(__method__) unless block_given?
59
-
60
- @templates.each do |template|
61
- locals_set = @locals_sets[template.virtual_path]
62
- if locals_set
63
- locals_set.each do |locals|
64
- yield template, locals
65
- end
66
- elsif !template.partial?
67
- # For now, guess that non-partials we haven't seen take no locals
68
- yield template, []
69
- else
70
- # Locals unknown
71
- end
47
+ def template_renders
48
+ return @template_renders if @template_renders
49
+
50
+ template_renders = []
51
+
52
+ @scanners.each do |scanner|
53
+ template_renders.concat scanner.template_renders
72
54
  end
55
+
56
+ template_renders.concat @static_templates
57
+
58
+ template_renders.uniq!
59
+
60
+ @template_renders = template_renders
73
61
  end
74
62
 
75
- def determine_locals
76
- @locals_sets = {}
77
-
78
- @templates.each do |template|
79
- parser = TemplateParser.new(template.fullpath)
80
- parser.render_calls.each do |render_call|
81
- virtual_path = render_call.virtual_path
82
- unless virtual_path.include?("/")
83
- # Not necessarily true, since the perfix is based on the current
84
- # controller, but is a safe bet most of the time.
85
- virtual_path = "#{template.prefix}/#{virtual_path}"
86
- end
87
- @locals_sets[virtual_path] ||= []
88
- @locals_sets[virtual_path] << render_call.locals_keys.map(&:to_s).sort
89
- end
90
- end
63
+ private
91
64
 
92
- @locals_sets.each_value(&:uniq!)
65
+ def debug(msg)
66
+ puts msg if @verbose
93
67
  end
94
68
  end
95
69
  end
@@ -1,50 +1,84 @@
1
1
  module ActionviewPrecompiler
2
- RenderCall = Struct.new(:render_type, :template, :locals, :locals_keys) do
3
- def virtual_path
4
- if render_type == :partial
5
- @virtual_path ||= template.gsub(%r{(/|^)([^/]*)\z}, '\1_\2')
6
- else
7
- template
8
- end
9
- end
10
- end
2
+ RenderCall = Struct.new(:virtual_path, :locals_keys)
11
3
 
12
4
  class RenderParser
13
- include ASTParser
14
-
15
- def initialize(code)
5
+ def initialize(code, parser: ASTParser, from_controller: false)
16
6
  @code = code
17
- @code = parse(code) if code.is_a?(String)
7
+ @parser = parser
8
+ @from_controller = from_controller
18
9
  end
19
10
 
20
11
  def render_calls
21
- render_nodes = extract_render_nodes(@code)
22
- render_nodes.map do |node|
23
- parse_render(node)
24
- end.compact
12
+ render_nodes = @parser.parse_render_nodes(@code)
13
+ render_nodes.map do |method, nodes|
14
+ parse_method = case method
15
+ when :layout
16
+ :parse_layout
17
+ else
18
+ :parse_render
19
+ end
20
+
21
+ nodes.map { |n| send(parse_method, n) }
22
+ end.flatten.compact
25
23
  end
26
24
 
27
25
  private
28
26
 
27
+ # Convert
28
+ # render("foo", ...)
29
+ # into either
30
+ # render(template: "foo", ...)
31
+ # or
32
+ # render(partial: "foo", ...)
33
+ # depending on controller or view context
34
+ def normalize_args(string, options_hash)
35
+ if @from_controller
36
+ if options_hash
37
+ options = parse_hash_to_symbols(options_hash)
38
+ else
39
+ options = {}
40
+ end
41
+ return nil unless options
42
+ options.merge(template: string)
43
+ else
44
+ if options_hash
45
+ { partial: string, locals: options_hash }
46
+ else
47
+ { partial: string }
48
+ end
49
+ end
50
+ end
51
+
29
52
  def parse_render(node)
30
53
  node = node.argument_nodes
31
- if (node.length == 1 || node.length == 2) && node[0].string?
32
- # FIXME: from template vs controller
33
- options = {}
34
- options[:partial] = node[0]
35
- if node.length == 2
54
+ if (node.length == 1 || node.length == 2) && !node[0].hash?
55
+ if node.length == 1
56
+ options = normalize_args(node[0], nil)
57
+ elsif node.length == 2
36
58
  return unless node[1].hash?
37
- options[:locals] = node[1]
59
+ options = normalize_args(node[0], node[1])
38
60
  end
61
+ return nil unless options
39
62
  return parse_render_from_options(options)
40
63
  elsif node.length == 1 && node[0].hash?
41
64
  options = parse_hash_to_symbols(node[0])
65
+ return nil unless options
42
66
  return parse_render_from_options(options)
43
67
  else
44
68
  nil
45
69
  end
46
70
  end
47
71
 
72
+ def parse_layout(node)
73
+ return nil unless from_controller?
74
+
75
+ template = parse_str(node.argument_nodes[0])
76
+ return nil unless template
77
+
78
+ virtual_path = layout_to_virtual_path(template)
79
+ RenderCall.new(virtual_path, [])
80
+ end
81
+
48
82
  def parse_hash(node)
49
83
  node.hash? && node.to_hash
50
84
  end
@@ -52,22 +86,28 @@ module ActionviewPrecompiler
52
86
  def parse_hash_to_symbols(node)
53
87
  hash = parse_hash(node)
54
88
  return unless hash
55
- hash.transform_keys do |node|
56
- key = parse_sym(node)
89
+ hash.transform_keys do |key_node|
90
+ key = parse_sym(key_node)
57
91
  return unless key
58
92
  key
59
93
  end
60
94
  end
61
95
 
62
- RENDER_TYPE_KEYS = [:partial, :template, :layout]
63
- IGNORED_KEYS = [:formats]
64
- ALL_KNOWN_KEYS = [*RENDER_TYPE_KEYS, *IGNORED_KEYS, :locals, :object, :collection, :as]
96
+ ALL_KNOWN_KEYS = [:partial, :template, :layout, :formats, :locals, :object, :collection, :as, :status, :content_type, :location, :spacer_template]
65
97
 
66
98
  def parse_render_from_options(options_hash)
99
+ renders = []
67
100
  keys = options_hash.keys
68
101
 
69
- unless (keys & RENDER_TYPE_KEYS).one?
70
- # Must have one of partial:, template:, or layout:
102
+ render_type_keys =
103
+ if from_controller?
104
+ [:partial, :template]
105
+ else
106
+ [:partial, :template, :layout]
107
+ end
108
+
109
+ if (keys & render_type_keys).size < 1
110
+ # Must have at least one of render keys
71
111
  return nil
72
112
  end
73
113
 
@@ -76,8 +116,26 @@ module ActionviewPrecompiler
76
116
  return nil
77
117
  end
78
118
 
79
- render_type = (keys & RENDER_TYPE_KEYS)[0]
80
- template = parse_str(options_hash[render_type])
119
+ render_type = (keys & render_type_keys)[0]
120
+
121
+ node = options_hash[render_type]
122
+ if node.string?
123
+ template = node.to_string
124
+ else
125
+ if node.variable_reference?
126
+ dependency = node.variable_name.sub(/\A(?:\$|@{1,2})/, "")
127
+ elsif node.vcall?
128
+ dependency = node.variable_name
129
+ elsif node.call?
130
+ dependency = node.call_method_name
131
+ return unless dependency.is_a? String
132
+ else
133
+ return
134
+ end
135
+ object_template = true
136
+ template = "#{dependency.pluralize}/#{dependency.singularize}"
137
+ end
138
+
81
139
  return unless template
82
140
 
83
141
  if options_hash.key?(:locals)
@@ -93,7 +151,13 @@ module ActionviewPrecompiler
93
151
  locals_keys = []
94
152
  end
95
153
 
96
- if options_hash.key?(:object) || options_hash.key?(:collection)
154
+ if spacer_template = render_template_with_spacer?(options_hash)
155
+ virtual_path = partial_to_virtual_path(:partial, spacer_template)
156
+ # Locals keys should not include collection keys
157
+ renders << RenderCall.new(virtual_path, locals_keys.dup)
158
+ end
159
+
160
+ if options_hash.key?(:object) || options_hash.key?(:collection) || object_template
97
161
  return nil if options_hash.key?(:object) && options_hash.key?(:collection)
98
162
  return nil unless options_hash.key?(:partial)
99
163
 
@@ -112,7 +176,28 @@ module ActionviewPrecompiler
112
176
  end
113
177
  end
114
178
 
115
- RenderCall.new(render_type, template, locals, locals_keys)
179
+ virtual_path = partial_to_virtual_path(render_type, template)
180
+ renders << RenderCall.new(virtual_path, locals_keys)
181
+
182
+ # Support for rendering multiple templates (i.e. a partial with a layout)
183
+ if layout_template = render_template_with_layout?(render_type, options_hash)
184
+ virtual_path = if from_controller?
185
+ layout_to_virtual_path(layout_template)
186
+ else
187
+ if !layout_template.include?("/") &&
188
+ partial_prefix = template.match(%r{(.*)/([^/]*)\z})
189
+ # TODO: use the file path that this render call was found in to
190
+ # generate the partial prefix instead of rendered partial.
191
+ partial_prefix = partial_prefix[1]
192
+ layout_template = "#{partial_prefix}/#{layout_template}"
193
+ end
194
+ partial_to_virtual_path(:layout, layout_template)
195
+ end
196
+
197
+ renders << RenderCall.new(virtual_path, locals_keys)
198
+ end
199
+
200
+ renders
116
201
  end
117
202
 
118
203
  def parse_str(node)
@@ -123,21 +208,38 @@ module ActionviewPrecompiler
123
208
  node.symbol? && node.to_symbol
124
209
  end
125
210
 
211
+ private
212
+
126
213
  def debug(message)
127
214
  warn message
128
215
  end
129
216
 
130
- def extract_render_nodes(node)
131
- return [] unless node?(node)
132
- renders = node.children.flat_map { |c| extract_render_nodes(c) }
133
- if render_call?(node)
134
- renders << node
217
+ def from_controller?
218
+ @from_controller
219
+ end
220
+
221
+ def render_template_with_layout?(render_type, options_hash)
222
+ if render_type != :layout && options_hash.key?(:layout)
223
+ parse_str(options_hash[:layout])
224
+ end
225
+ end
226
+
227
+ def render_template_with_spacer?(options_hash)
228
+ if !from_controller? && options_hash.key?(:spacer_template)
229
+ parse_str(options_hash[:spacer_template])
230
+ end
231
+ end
232
+
233
+ def partial_to_virtual_path(render_type, partial_path)
234
+ if render_type == :partial || render_type == :layout
235
+ partial_path.gsub(%r{(/|^)([^/]*)\z}, '\1_\2')
236
+ else
237
+ partial_path
135
238
  end
136
- renders
137
239
  end
138
240
 
139
- def render_call?(node)
140
- fcall?(node, :render)
241
+ def layout_to_virtual_path(layout_path)
242
+ "layouts/#{layout_path}"
141
243
  end
142
244
  end
143
245
  end
@@ -0,0 +1,22 @@
1
+ module ActionviewPrecompiler
2
+ class TemplateFile
3
+ attr_reader :fullpath, :relative_path, :virtual_path
4
+ attr_reader :action, :prefix, :details
5
+
6
+ def initialize(fullpath, relative_path)
7
+ @fullpath = fullpath
8
+ @relative_path = relative_path
9
+ @virtual_path = relative_path.slice(0, relative_path.index("."))
10
+
11
+ parsed = ParsedFilename.new(relative_path)
12
+ @prefix = parsed.prefix
13
+ @action = parsed.action
14
+ @partial = parsed.partial?
15
+ @details = parsed.details
16
+ end
17
+
18
+ def partial?
19
+ @partial
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,36 @@
1
+ module ActionviewPrecompiler
2
+ class TemplateLoader
3
+ VIRTUAL_PATH_REGEX = %r{\A(?:(?<prefix>.*)\/)?(?<partial>_)?(?<action>[^\/\.]+)}
4
+
5
+ def initialize
6
+ target = ActionController::Base
7
+ @lookup_context = ActionView::LookupContext.new(target.view_paths)
8
+ @view_context_class = target.view_context_class
9
+ end
10
+
11
+ def load_template(virtual_path, locals)
12
+ templates = find_all_templates(virtual_path, locals)
13
+ templates.each do |template|
14
+ template.send(:compile!, @view_context_class)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def find_all_templates(virtual_path, locals)
21
+ match = virtual_path.match(VIRTUAL_PATH_REGEX)
22
+ if match
23
+ action = match[:action]
24
+ prefix = match[:prefix] ? [match[:prefix]] : []
25
+ partial = !!match[:partial]
26
+
27
+ # Assume templates with different details take same locals
28
+ details = {}
29
+
30
+ @lookup_context.find_all(action, prefix, partial, locals, details)
31
+ else
32
+ []
33
+ end
34
+ end
35
+ end
36
+ end
@@ -2,8 +2,6 @@ require "action_view"
2
2
 
3
3
  module ActionviewPrecompiler
4
4
  class TemplateParser
5
- include ASTParser
6
-
7
5
  attr_reader :filename, :basename, :handler
8
6
 
9
7
  class FakeTemplate
@@ -24,24 +22,18 @@ module ActionviewPrecompiler
24
22
  @filename = filename
25
23
  @basename = File.basename(filename)
26
24
  handler_ext = @basename.split(".").last
27
- @handler = ActionView::Template.handler_for_extension(handler_ext)
28
- @is_partial = !!@basename.start_with?("_")
29
- end
30
-
31
- def partial?
32
- @is_partial
25
+ @handler = HANDLERS_FOR_EXTENSION[handler_ext]
33
26
  end
34
27
 
35
28
  def render_calls
36
- RenderParser.new(parsed).render_calls
37
- end
38
-
39
- def parsed
40
- @parsed ||= parse(compiled_source)
41
- end
42
-
43
- def compiled_source
44
- @handler.call(FakeTemplate.new, File.read(@filename))
29
+ src = File.read(@filename)
30
+ if src.include?("render")
31
+ compiled_source = @handler.call(FakeTemplate.new, File.read(@filename))
32
+ compiled_source = "def _template; #{compiled_source}; end"
33
+ RenderParser.new(compiled_source).render_calls
34
+ else
35
+ []
36
+ end
45
37
  end
46
38
  end
47
39
  end
@@ -0,0 +1,44 @@
1
+ require "actionview_precompiler/template_file"
2
+
3
+ module ActionviewPrecompiler
4
+ class TemplateScanner
5
+ attr_reader :view_dir
6
+
7
+ def initialize(view_dir)
8
+ @view_dir = view_dir
9
+ end
10
+
11
+ def template_renders
12
+ template_renders = []
13
+
14
+ each_template do |template|
15
+ parser = TemplateParser.new(template.fullpath)
16
+ parser.render_calls.each do |render_call|
17
+ virtual_path = render_call.virtual_path
18
+ unless virtual_path.include?("/")
19
+ # Not necessarily true, since the perfix is based on the current
20
+ # controller, but is a safe bet most of the time.
21
+ virtual_path = "#{template.prefix}/#{virtual_path}"
22
+ end
23
+
24
+ locals = render_call.locals_keys.map(&:to_s).sort
25
+
26
+ template_renders << [virtual_path, locals]
27
+ end
28
+ end
29
+
30
+ template_renders.uniq
31
+ end
32
+
33
+ private
34
+
35
+ def each_template
36
+ Dir["**/*", base: view_dir].sort.map do |file|
37
+ fullpath = File.expand_path(file, view_dir)
38
+ next if File.directory?(fullpath)
39
+
40
+ yield TemplateFile.new(fullpath, file)
41
+ end.compact
42
+ end
43
+ end
44
+ end
@@ -1,3 +1,3 @@
1
1
  module ActionviewPrecompiler
2
- VERSION = "0.2.3"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -1,33 +1,47 @@
1
+ require "action_controller"
2
+ require "action_view"
3
+
1
4
  require "actionview_precompiler/version"
2
5
  require "actionview_precompiler/ast_parser"
3
6
  require "actionview_precompiler/template_parser"
4
7
  require "actionview_precompiler/render_parser"
8
+ require "actionview_precompiler/controller_parser"
9
+ require "actionview_precompiler/helper_parser"
5
10
  require "actionview_precompiler/precompiler"
6
11
  require "actionview_precompiler/parsed_filename"
7
12
 
8
13
  module ActionviewPrecompiler
9
14
  class Error < StandardError; end
10
15
 
16
+ # Allow overriding from ActionView default handlers if necessary
17
+ HANDLERS_FOR_EXTENSION = Hash.new do |h, ext|
18
+ h[ext] = ActionView::Template.handler_for_extension(ext)
19
+ end
20
+
11
21
  def self.precompile(verbose: false)
12
- target = ActionController::Base # fixme
13
- view_paths = target.view_paths
14
- lookup_context = ActionView::LookupContext.new(view_paths)
15
- paths = view_paths.map(&:path)
16
- precompiler = Precompiler.new(paths)
17
-
18
- mod = target.view_context_class
19
- count = 0
20
- precompiler.each_lookup_args do |args|
21
- templates = lookup_context.find_all(*args)
22
- templates.each do |template|
23
- puts "precompiling: #{template.inspect}" if verbose
24
- count += 1
25
- template.send(:compile!, mod)
22
+ precompiler = Precompiler.new(verbose: verbose)
23
+
24
+ if block_given?
25
+ # Custom configuration
26
+ yield precompiler
27
+ else
28
+ # Scan view dirs
29
+ ActionController::Base.view_paths.each do |view_path|
30
+ precompiler.scan_view_dir view_path.path
26
31
  end
27
- end
28
32
 
29
- if verbose
30
- puts "Precompiled #{count} Templates"
33
+ # If we have an application, scan controllers
34
+ if Rails.respond_to?(:application)
35
+ Rails.application.paths["app/controllers"].each do |path|
36
+ precompiler.scan_controller_dir path.to_s
37
+ end
38
+
39
+ Rails.application.paths["app/helpers"].each do |path|
40
+ precompiler.scan_helper_dir path.to_s
41
+ end
42
+ end
31
43
  end
44
+
45
+ precompiler.run
32
46
  end
33
47
  end