actionview_precompiler 0.2.3 → 0.4.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,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