actionview_precompiler 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.
@@ -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,25 @@ 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
+ else
132
+ return
133
+ end
134
+ object_template = true
135
+ template = "#{dependency.pluralize}/#{dependency.singularize}"
136
+ end
137
+
81
138
  return unless template
82
139
 
83
140
  if options_hash.key?(:locals)
@@ -93,7 +150,13 @@ module ActionviewPrecompiler
93
150
  locals_keys = []
94
151
  end
95
152
 
96
- if options_hash.key?(:object) || options_hash.key?(:collection)
153
+ if spacer_template = render_template_with_spacer?(options_hash)
154
+ virtual_path = partial_to_virtual_path(:partial, spacer_template)
155
+ # Locals keys should not include collection keys
156
+ renders << RenderCall.new(virtual_path, locals_keys.dup)
157
+ end
158
+
159
+ if options_hash.key?(:object) || options_hash.key?(:collection) || object_template
97
160
  return nil if options_hash.key?(:object) && options_hash.key?(:collection)
98
161
  return nil unless options_hash.key?(:partial)
99
162
 
@@ -112,7 +175,28 @@ module ActionviewPrecompiler
112
175
  end
113
176
  end
114
177
 
115
- RenderCall.new(render_type, template, locals, locals_keys)
178
+ virtual_path = partial_to_virtual_path(render_type, template)
179
+ renders << RenderCall.new(virtual_path, locals_keys)
180
+
181
+ # Support for rendering multiple templates (i.e. a partial with a layout)
182
+ if layout_template = render_template_with_layout?(render_type, options_hash)
183
+ virtual_path = if from_controller?
184
+ layout_to_virtual_path(layout_template)
185
+ else
186
+ if !layout_template.include?("/") &&
187
+ partial_prefix = template.match(%r{(.*)/([^/]*)\z})
188
+ # TODO: use the file path that this render call was found in to
189
+ # generate the partial prefix instead of rendered partial.
190
+ partial_prefix = partial_prefix[1]
191
+ layout_template = "#{partial_prefix}/#{layout_template}"
192
+ end
193
+ partial_to_virtual_path(:layout, layout_template)
194
+ end
195
+
196
+ renders << RenderCall.new(virtual_path, locals_keys)
197
+ end
198
+
199
+ renders
116
200
  end
117
201
 
118
202
  def parse_str(node)
@@ -123,21 +207,38 @@ module ActionviewPrecompiler
123
207
  node.symbol? && node.to_symbol
124
208
  end
125
209
 
210
+ private
211
+
126
212
  def debug(message)
127
213
  warn message
128
214
  end
129
215
 
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
216
+ def from_controller?
217
+ @from_controller
218
+ end
219
+
220
+ def render_template_with_layout?(render_type, options_hash)
221
+ if render_type != :layout && options_hash.key?(:layout)
222
+ parse_str(options_hash[:layout])
223
+ end
224
+ end
225
+
226
+ def render_template_with_spacer?(options_hash)
227
+ if !from_controller? && options_hash.key?(:spacer_template)
228
+ parse_str(options_hash[:spacer_template])
229
+ end
230
+ end
231
+
232
+ def partial_to_virtual_path(render_type, partial_path)
233
+ if render_type == :partial || render_type == :layout
234
+ partial_path.gsub(%r{(/|^)([^/]*)\z}, '\1_\2')
235
+ else
236
+ partial_path
135
237
  end
136
- renders
137
238
  end
138
239
 
139
- def render_call?(node)
140
- fcall?(node, :render)
240
+ def layout_to_virtual_path(layout_path)
241
+ "layouts/#{layout_path}"
141
242
  end
142
243
  end
143
244
  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
@@ -14,30 +12,27 @@ module ActionviewPrecompiler
14
12
  def type
15
13
  nil
16
14
  end
15
+
16
+ def format
17
+ nil
18
+ end
17
19
  end
18
20
 
19
21
  def initialize(filename)
20
22
  @filename = filename
21
23
  @basename = File.basename(filename)
22
24
  handler_ext = @basename.split(".").last
23
- @handler = ActionView::Template.handler_for_extension(handler_ext)
24
- @is_partial = !!@basename.start_with?("_")
25
- end
26
-
27
- def partial?
28
- @is_partial
25
+ @handler = HANDLERS_FOR_EXTENSION[handler_ext]
29
26
  end
30
27
 
31
28
  def render_calls
32
- RenderParser.new(parsed).render_calls
33
- end
34
-
35
- def parsed
36
- @parsed ||= parse(compiled_source)
37
- end
38
-
39
- def compiled_source
40
- @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
+ RenderParser.new(compiled_source).render_calls
33
+ else
34
+ []
35
+ end
41
36
  end
42
37
  end
43
38
  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.2"
2
+ VERSION = "0.3.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