slinky 0.7.3 → 0.8.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,113 @@
1
+ # coding: utf-8
2
+ module Slinky
3
+ # The Graph class describes a directed graph and provides various
4
+ # graph algorithms.
5
+ class Graph
6
+ include Enumerable
7
+
8
+ attr_reader :nodes, :edges
9
+
10
+ # Creates a new Graph from an adjacency list
11
+ def initialize nodes, edges
12
+ @nodes = nodes
13
+ @edges = edges
14
+ end
15
+
16
+ # Builds an adjacency matrix representation of the graph
17
+ def adjacency_matrix
18
+ return @adjacency_matrix if @adjacency_matrix
19
+
20
+ # Convert from adjacency list to a map structure
21
+ g = Hash.new{|h,k| h[k] = []}
22
+ edges.each{|x|
23
+ g[x[1]] << x[0]
24
+ }
25
+
26
+ @adjacency_matrix = g
27
+ end
28
+
29
+ # Builds the transitive closure of the dependency graph using
30
+ # Floyd–Warshall
31
+ def transitive_closure
32
+ return @transitive_closure if @transitive_closure
33
+
34
+ g = adjacency_matrix
35
+
36
+ index_map = {}
37
+ nodes.each_with_index{|f, i| index_map[f] = i}
38
+
39
+ size = nodes.size
40
+
41
+ # Set up the distance matrix
42
+ dist = Array.new(size){|_| Array.new(size, Float::INFINITY)}
43
+ nodes.each_with_index{|fi, i|
44
+ dist[i][i] = 0
45
+ g[fi].each{|fj|
46
+ dist[i][index_map[fj]] = 1
47
+ }
48
+ }
49
+
50
+ # Compute the all-paths costs
51
+ size.times{|k|
52
+ size.times{|i|
53
+ size.times{|j|
54
+ if dist[i][j] > dist[i][k] + dist[k][j]
55
+ dist[i][j] = dist[i][k] + dist[k][j]
56
+ end
57
+ }
58
+ }
59
+ }
60
+
61
+ # Compute the transitive closure in map form
62
+ @transitive_closure = Hash.new{|h,k| h[k] = []}
63
+ size.times{|i|
64
+ size.times{|j|
65
+ if dist[i][j] < Float::INFINITY
66
+ @transitive_closure[nodes[i]] << nodes[j]
67
+ end
68
+ }
69
+ }
70
+
71
+ @transitive_closure
72
+ end
73
+
74
+ # Builds a list of files in topological order, so that when
75
+ # required in this order all dependencies are met. See
76
+ # http://en.wikipedia.org/wiki/Topological_sorting for more
77
+ # information.
78
+ def dependency_list
79
+ return @dependency_list if @dependency_list
80
+
81
+ graph = edges.clone
82
+ # will contain sorted elements
83
+ l = []
84
+ # start nodes, those with no incoming edges
85
+ s = nodes.reject{|mf| mf.directives[:slinky_require]}
86
+ while s.size > 0
87
+ n = s.delete s.first
88
+ l << n
89
+ nodes.each{|m|
90
+ e = graph.find{|e| e[0] == n && e[1] == m}
91
+ next unless e
92
+ graph.delete e
93
+ s << m unless graph.any?{|e| e[1] == m}
94
+ }
95
+ end
96
+ if graph != []
97
+ problems = graph.collect{|e| e.collect{|x| x.source}.join(" -> ")}
98
+ raise DependencyError.new("Dependencies #{problems.join(", ")} could not be satisfied")
99
+ end
100
+ @dependency_list = l
101
+ end
102
+
103
+ def each &block
104
+ edges.each do |e|
105
+ if block_given?
106
+ block.call e
107
+ else
108
+ yield e
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -1,30 +1,26 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  require 'pathname'
3
3
  require 'digest/md5'
4
+ require 'matrix'
5
+ require 'set'
4
6
 
5
7
  module Slinky
6
- # extensions of files that can contain build directives
7
- DIRECTIVE_FILES = %w{js css html haml sass scss coffee}
8
+ # extensions of non-compiled files that can contain build directives
9
+ DIRECTIVE_FILES = %w{js css html}
8
10
  DEPENDS_DIRECTIVE = /^[^\n\w]*(slinky_depends)\((".*"|'.+'|)\)[^\n\w]*$/
9
11
  REQUIRE_DIRECTIVE = /^[^\n\w]*(slinky_require)\((".*"|'.+'|)\)[^\n\w]*$/
10
12
  SCRIPTS_DIRECTIVE = /^[^\n\w]*(slinky_scripts)[^\n\w]*$/
11
13
  STYLES_DIRECTIVE = /^[^\n\w]*(slinky_styles)[^\n\w]*$/
14
+ PRODUCT_DIRECTIVE = /^[^\n\w]*(slinky_product)\((".*"|'.+'|)\)[^\n\w]*$/
12
15
  BUILD_DIRECTIVES = Regexp.union(DEPENDS_DIRECTIVE,
13
16
  REQUIRE_DIRECTIVE,
14
17
  SCRIPTS_DIRECTIVE,
15
- STYLES_DIRECTIVE)
18
+ STYLES_DIRECTIVE,
19
+ PRODUCT_DIRECTIVE)
16
20
  CSS_URL_MATCHER = /url\(['"]?([^'"\/][^\s)]+\.[a-z]+)(\?\d+)?['"]?\)/
17
21
 
18
- # Raised when a compilation fails for any reason
19
- class BuildFailedError < StandardError; end
20
- # Raised when a required file is not found.
21
- class FileNotFoundError < StandardError; end
22
- # Raised when there is a cycle in the dependency graph (i.e., file A
23
- # requires file B which requires C which requires A)
24
- class DependencyError < StandardError; end
25
-
26
22
  class Manifest
27
- attr_accessor :manifest_dir, :dir
23
+ attr_accessor :manifest_dir, :dir, :config
28
24
 
29
25
  def initialize dir, config, options = {}
30
26
  @dir = dir
@@ -87,59 +83,170 @@ module Slinky
87
83
  @manifest_dir.find_by_path path, allow_multiple
88
84
  end
89
85
 
90
- def scripts_string
91
- if @devel
92
- dependency_list.reject{|x| x.output_path.extname != ".js" }.collect{|d|
93
- %Q\<script type="text/javascript" src="/#{d.relative_output_path}"></script>\
94
- }.join("\n")
95
- else
96
- %Q\<script type="text/javascript" src="/scripts.js?#{rand(999999999)}"></script>\
86
+ # Finds all files that match the given pattern. The match rules
87
+ # are similar to those for .gitignore and given by
88
+ #
89
+ # 1. If the pattern ends with a slash, it will only match directories;
90
+ # e.g. `foo/` would match a directory `foo/` but not a file `foo`. In
91
+ # a file context, matching a directory is equivalent to matching all
92
+ # files under that directory, recursively.
93
+ # 2. If the pattern does not contain a slash, slinky treats it as a
94
+ # relative pathname which can match files in any directory. For
95
+ # example, the rule `test.js` will matching `/test.js` and
96
+ # `/component/test.js`.
97
+ # 3. If the pattern begins with a slash, it will be treated as an
98
+ # absolute path starting at the root of the source directory.
99
+ # 4. If the pattern does not begin with a slash, but does contain one or
100
+ # more slashes, it will be treated as a path relative to any
101
+ # directory. For example, `test/*.js` will match `/test/main.js`, and
102
+ # /component/test/component.js`, but not `main.js`.
103
+ # 5. A single star `*` in a pattern will match any number of characters within a
104
+ # single path component. For example, `/test/*.js` will match
105
+ # `/test/main_test.js` but not `/test/component/test.js`.
106
+ # 6. A double star `**` will match any number of characters including
107
+ # path separators. For example `/scripts/**/main.js` will match any
108
+ # file named `main.js` under the `/scripts` directory, including
109
+ # `/scripts/main.js` and `/scripts/component/main.js`.
110
+ def find_by_pattern pattern
111
+ # The strategy here is to convert the pattern into an equivalent
112
+ # regex and run that against the pathnames of all the files in
113
+ # the manifest.
114
+ regex_str = Regexp.escape(pattern)
115
+ .gsub('\*\*/', ".*")
116
+ .gsub('\*\*', ".*")
117
+ .gsub('\*', "[^/]*")
118
+
119
+ if regex_str[0] != '/'
120
+ regex_str = '.*/' + regex_str
121
+ end
122
+
123
+ if regex_str[-1] == '/'
124
+ regex_str += '.*'
97
125
  end
126
+
127
+ regex_str = "^#{regex_str}$"
128
+
129
+ regex = Regexp.new(regex_str)
130
+
131
+ files(false).reject{|f|
132
+ !regex.match('/' + f.relative_source_path.to_s) &&
133
+ !regex.match('/' + f.relative_output_path.to_s)
134
+ }
98
135
  end
99
136
 
100
- def compress ext, output, compressor
101
- scripts = dependency_list.reject{|x| x.output_path.extname != ext}
137
+ # Finds all the matching manifest files for a particular product.
138
+ # This does not take into account dependencies.
139
+ def files_for_product product
140
+ if !p = @config.produce[product]
141
+ SlinkyError.raise NoSuchProductError,
142
+ "Product '#{product}' has not been configured"
143
+ end
102
144
 
103
- if scripts.size > 0
104
- s = scripts.collect{|s|
105
- f = File.open(s.build_to.to_s, 'rb'){|f| f.read}
106
- (block_given?) ? (yield s, f) : f
107
- }.join("\n")
145
+ type = type_for_product product
146
+ if type != ".js" && type != ".css"
147
+ SlinkyError.raise InvalidConfigError, "Only .js and .css products are supported"
148
+ end
108
149
 
109
- File.open(output, "w+"){|f|
110
- unless @no_minify
111
- f.write(compressor.compress(s))
112
- else
113
- f.write(s)
150
+ g = dependency_graph.transitive_closure
151
+
152
+ # Topological indices for each file
153
+ indices = {}
154
+ dependency_list.each_with_index{|f, i| indices[f] = i}
155
+
156
+ # Compute the set of excluded files
157
+ excludes = Set.new((p["exclude"] || []).map{|p|
158
+ find_by_pattern(p)
159
+ }.flatten.uniq)
160
+
161
+ SlinkyError.batch_errors do
162
+ # First find the list of files that have been explictly
163
+ # included/excluded
164
+ p["include"].map{|f|
165
+ mfs = find_by_pattern(f)
166
+ .map{|mf| [mf] + g[f]}
167
+ .flatten
168
+ .reject{|f| f.output_path.extname != type}
169
+ if mfs.empty?
170
+ SlinkyError.raise FileNotFoundError,
171
+ "No files matched by include #{f} in product #{product}"
114
172
  end
173
+ mfs.flatten
174
+ }.flatten.reject{|f|
175
+ excludes.include?(f)
176
+ # Then add all the files these require
177
+ }.map{|f|
178
+ # Find all of the downstream files
179
+ # check that we're not excluding any required files
180
+ g[f].each{|rf|
181
+ if p["exclude"] && r = p["exclude"].find{|ex| rf.matches_path?(ex, true)}
182
+ SlinkyError.raise DependencyError,
183
+ "File #{f} requires #{rf} which is excluded by exclusion rule #{r}"
184
+ end
185
+ }
186
+ [f] + g[f]
187
+ }.flatten.uniq.sort_by{|f|
188
+ # Sort by topological order
189
+ indices[f]
115
190
  }
116
- scripts.collect{|s| FileUtils.rm(s.build_to)}
117
191
  end
118
192
  end
119
193
 
120
- def compress_scripts
121
- compressor = YUI::JavaScriptCompressor.new(:munge => false)
122
- compress(".js", "#{@build_to}/scripts.js", compressor)
194
+ def files_for_all_products
195
+ return @files_for_all_products if @files_for_all_products
196
+ SlinkyError.batch_errors do
197
+ @files_for_all_products = @config.produce.keys.map{|product|
198
+ files_for_product(product)
199
+ }.flatten.uniq
200
+ end
123
201
  end
124
202
 
125
- def compress_styles
126
- compressor = YUI::CssCompressor.new()
203
+ def compress_product product
204
+ compressor = compressor_for_product product
205
+ post_processor = post_processor_for_product product
127
206
 
128
- compress(".css", "#{@build_to}/styles.css", compressor){|s, css|
129
- css.gsub(CSS_URL_MATCHER){|url|
130
- p = s.relative_output_path.dirname.to_s + "/#{$1}"
131
- "url('/#{p}')"
132
- }
207
+ s = files_for_product(product).map{|mf|
208
+ f = File.open(mf.build_to.to_s, 'rb'){|f| f.read}
209
+ post_processor ? (post_processor.call(mf, f)) : f
210
+ }.join("\n")
211
+
212
+ # Make the directory the product is in
213
+ FileUtils.mkdir_p("#{@build_to}/#{Pathname.new(product).dirname}")
214
+ File.open("#{@build_to}/#{product}", "w+"){|f|
215
+ unless @no_minify
216
+ f.write(compressor.compress(s))
217
+ else
218
+ f.write(s)
219
+ end
133
220
  }
134
221
  end
135
222
 
223
+ # These are special cases for simplicity and backwards
224
+ # compatability. If no products are defined, we have two default
225
+ # products, one which includes are .js files in the repo and one
226
+ # that includes all .css files. This method produces an HTML include
227
+ # string for all of the .js files.
228
+ def scripts_string
229
+ product_string ConfigReader::DEFAULT_SCRIPT_PRODUCT
230
+ end
231
+
232
+ # These are special cases for simplicity and backwards
233
+ # compatability. If no products are defined, we have two default
234
+ # products, one which includes are .js files in the repo and one
235
+ # that includes all .css files. This method produces an HTML include
236
+ # string for all of the .css files.
136
237
  def styles_string
238
+ product_string ConfigReader::DEFAULT_STYLE_PRODUCT
239
+ end
240
+
241
+ # Produces a string of HTML that includes all of the files for the
242
+ # given product.
243
+ def product_string product
137
244
  if @devel
138
- dependency_list.reject{|x| x.output_path.extname != ".css"}.collect{|d|
139
- %Q\<link rel="stylesheet" href="/#{d.relative_output_path}" />\
245
+ files_for_product(product).map{|f|
246
+ html_for_path("/#{f.relative_output_path}")
140
247
  }.join("\n")
141
248
  else
142
- %Q\<link rel="stylesheet" href="/styles.css?#{rand(999999999)}" />\
249
+ html_for_path("#{product}?#{rand(999999999)}")
143
250
  end
144
251
  end
145
252
 
@@ -149,59 +256,32 @@ module Slinky
149
256
  # (required, by), each of which describes an edge.
150
257
  #
151
258
  # @return [[ManifestFile, ManifestFile]] the graph
152
- def build_dependency_graph
259
+ def dependency_graph
260
+ return @dependency_graph if @dependency_graph
261
+
153
262
  graph = []
154
263
  files(false).each{|mf|
155
- mf.directives[:slinky_require].each{|rf|
156
- required = mf.parent.find_by_path(rf, true)
157
- if required.size > 0
158
- required.each{|x|
159
- graph << [x, mf]
160
- }
161
- else
162
- error = "Could not find file #{rf} required by #{mf.source}"
163
- $stderr.puts error.foreground(:red)
164
- raise FileNotFoundError.new(error)
165
- end
166
- } if mf.directives[:slinky_require]
264
+ mf.dependencies.each{|d|
265
+ graph << [d, mf]
266
+ }
167
267
  }
168
- @dependency_graph = graph
268
+
269
+ @dependency_graph = Graph.new(files(false), graph)
169
270
  end
170
271
 
171
- # Builds a list of files in topological order, so that when
172
- # required in this order all dependencies are met. See
173
- # http://en.wikipedia.org/wiki/Topological_sorting for more
174
- # information.
175
272
  def dependency_list
176
- build_dependency_graph unless @dependency_graph
177
- graph = @dependency_graph.clone
178
- # will contain sorted elements
179
- l = []
180
- # start nodes, those with no incoming edges
181
- s = files(false).reject{|mf| mf.directives[:slinky_require]}
182
- while s.size > 0
183
- n = s.delete s.first
184
- l << n
185
- files(false).each{|m|
186
- e = graph.find{|e| e[0] == n && e[1] == m}
187
- next unless e
188
- graph.delete e
189
- s << m unless graph.any?{|e| e[1] == m}
190
- }
191
- end
192
- if graph != []
193
- problems = graph.collect{|e| e.collect{|x| x.source}.join(" -> ")}
194
- $stderr.puts "Dependencies #{problems.join(", ")} could not be satisfied".foreground(:red)
195
- raise DependencyError
196
- end
197
- l
273
+ dependency_graph.dependency_list
198
274
  end
199
275
 
200
276
  def build
201
277
  @manifest_dir.build
202
278
  unless @devel
203
- compress_scripts
204
- compress_styles
279
+ @config.produce.keys.each{|product|
280
+ compress_product(product)
281
+ }
282
+
283
+ # clean up the files that have been processed
284
+ files_for_all_products.each{|mf| FileUtils.rm(mf.build_to, :force => true)}
205
285
  end
206
286
  end
207
287
 
@@ -225,10 +305,46 @@ module Slinky
225
305
  end
226
306
  end
227
307
 
308
+ def compressor_for_product product
309
+ case type_for_product(product)
310
+ when ".js"
311
+ YUI::JavaScriptCompressor.new(:munge => false)
312
+ when ".css"
313
+ YUI::CssCompressor.new()
314
+ end
315
+ end
316
+
317
+ def post_processor_for_product product
318
+ case type_for_product(product)
319
+ when ".css"
320
+ lambda{|s, css| css.gsub(CSS_URL_MATCHER){|url|
321
+ p = s.relative_output_path.dirname.to_s + "/#{$1}"
322
+ "url('/#{p}')"
323
+ }}
324
+ end
325
+ end
326
+
228
327
  def invalidate_cache
229
328
  @files = nil
230
329
  @dependency_graph = nil
231
330
  @md5 = nil
331
+ @files_for_all_products = nil
332
+ end
333
+
334
+ def html_for_path path
335
+ ext = path.split("?").first.split(".").last
336
+ case ext
337
+ when "css"
338
+ %Q|<link rel="stylesheet" href="#{path}" />|
339
+ when "js"
340
+ %Q|<script type="text/javascript" src="#{path}"></script>|
341
+ else
342
+ raise InvalidConfigError.new("Unsupported file extension #{ext}")
343
+ end
344
+ end
345
+
346
+ def type_for_product product
347
+ "." + product.split(".")[-1]
232
348
  end
233
349
 
234
350
  def manifest_update paths
@@ -278,6 +394,11 @@ module Slinky
278
394
  #
279
395
  # @return [ManifestFile] the manifest file at that path if one exists
280
396
  def find_by_path path, allow_multiple = false
397
+ if path[0] == '/'
398
+ # refer absolute paths to the manifest
399
+ return @manifest.find_by_path(path[1..-1], allow_multiple)
400
+ end
401
+
281
402
  components = path.to_s.split(File::SEPARATOR).reject{|x| x == ""}
282
403
  case components.size
283
404
  when 0
@@ -351,9 +472,13 @@ module Slinky
351
472
  unless File.directory?(@build_dir.to_s)
352
473
  FileUtils.mkdir(@build_dir.to_s)
353
474
  end
354
- (@files + @children).each{|m|
355
- m.build
356
- }
475
+
476
+ if (@files + @children).map {|m| m.build}.any?
477
+ @build_dir
478
+ else
479
+ FileUtils.rmdir(@build_dir.to_s)
480
+ nil
481
+ end
357
482
  end
358
483
 
359
484
  def to_s
@@ -383,6 +508,26 @@ module Slinky
383
508
  @last_md5 = nil
384
509
  end
385
510
 
511
+ # Gets the list of manifest files that this one depends on
512
+ # according to its directive list and the dependencies config
513
+ # option.
514
+ #
515
+ # Throws a FileNotFoundError if a dependency doesn't exist in the
516
+ # tree.
517
+ def dependencies
518
+ SlinkyError.batch_errors do
519
+ (@directives[:slinky_require].to_a +
520
+ @manifest.config.dependencies["/" + relative_source_path.to_s].to_a).map{|rf|
521
+ required = parent.find_by_path(rf, true).flatten
522
+ if required.empty?
523
+ error = "Could not find file #{rf} required by /#{relative_source_path}"
524
+ SlinkyError.raise FileNotFoundError, error
525
+ end
526
+ required
527
+ }.flatten
528
+ end
529
+ end
530
+
386
531
  # Predicate which determines whether the supplied name is the same
387
532
  # as the file's name, taking into account compiled file
388
533
  # extensions. For example, if mf refers to "/tmp/test/hello.sass",
@@ -415,6 +560,16 @@ module Slinky
415
560
  end
416
561
  end
417
562
 
563
+ # Predicate which determines whether the file matches (see
564
+ # `ManifestFile#matches?`) the full path relative to the manifest
565
+ # root.
566
+ def matches_path? s, match_glob = false
567
+ p = Pathname.new(s)
568
+ dir = Pathname.new("/" + relative_source_path.to_s).dirname
569
+ matches?(p.basename.to_s, match_glob) &&
570
+ dir == p.dirname
571
+ end
572
+
418
573
  # Predicate which determines whether the file is the supplied path
419
574
  # or lies on supplied tree
420
575
  def in_tree? path
@@ -440,12 +595,12 @@ module Slinky
440
595
 
441
596
  # returns the source path relative to the manifest directory
442
597
  def relative_source_path
443
- Pathname.new(@source).relative_path_from Pathname.new(@manifest.dir)
598
+ Pathname.new(@source).relative_path_from(Pathname.new(@manifest.dir))
444
599
  end
445
600
 
446
601
  # Returns the output path relative to the manifest directory
447
602
  def relative_output_path
448
- output_path.relative_path_from Pathname.new(@manifest.dir)
603
+ output_path.relative_path_from(Pathname.new(@manifest.dir))
449
604
  end
450
605
 
451
606
  # Looks through the file for directives
@@ -482,8 +637,11 @@ module Slinky
482
637
  out = File.read(path)
483
638
  out.gsub!(DEPENDS_DIRECTIVE, "")
484
639
  out.gsub!(REQUIRE_DIRECTIVE, "")
485
- out.gsub!(SCRIPTS_DIRECTIVE, @manifest.scripts_string)
486
- out.gsub!(STYLES_DIRECTIVE, @manifest.styles_string)
640
+ out.gsub!(SCRIPTS_DIRECTIVE){ @manifest.scripts_string }
641
+ out.gsub!(STYLES_DIRECTIVE){ @manifest.styles_string }
642
+ out.gsub!(PRODUCT_DIRECTIVE){
643
+ @manifest.product_string($2[1..-2])
644
+ }
487
645
  to = to || Tempfile.new("slinky").path + ".cache"
488
646
  File.open(to, "w+"){|f|
489
647
  f.write(out)
@@ -528,27 +686,32 @@ module Slinky
528
686
  find_directives
529
687
  end
530
688
 
531
- depends = @directives[:slinky_depends].map{|f|
532
- p = parent.find_by_path(f, true)
533
- $stderr.puts "File #{f} depended on by #{@source} not found".foreground(:red) unless p.size > 0
534
- p
535
- }.flatten.compact if @directives[:slinky_depends]
536
- depends ||= []
537
- @processing = true
538
- # process each file on which we're dependent, watching out for
539
- # infinite loops
540
- depends.each{|f| f.process }
541
- @processing = false
542
-
543
- # get hash of source file
544
- if @last_path && hash == @last_md5 && depends.all?{|f| f.updated < start_time}
545
- @last_path
546
- else
547
- @last_md5 = hash
548
- @updated = Time.now
549
- # mangle file appropriately
550
- f = should_compile ? (compile @source) : @source
551
- @last_path = handle_directives(f, to)
689
+ SlinkyError.batch_errors do
690
+ depends = @directives[:slinky_depends].map{|f|
691
+ p = parent.find_by_path(f, true)
692
+ unless p.size > 0
693
+ SlinkyError.raise DependencyError,
694
+ "File #{f} dependedon by #{@source} not found"
695
+ end
696
+ p
697
+ }.flatten.compact if @directives[:slinky_depends]
698
+ depends ||= []
699
+ @processing = true
700
+ # process each file on which we're dependent, watching out for
701
+ # infinite loops
702
+ depends.each{|f| f.process }
703
+ @processing = false
704
+
705
+ # get hash of source file
706
+ if @last_path && hash == @last_md5 && depends.all?{|f| f.updated < start_time}
707
+ @last_path
708
+ else
709
+ @last_md5 = hash
710
+ @updated = Time.now
711
+ # mangle file appropriately
712
+ f = should_compile ? (compile @source) : @source
713
+ @last_path = handle_directives(f, to)
714
+ end
552
715
  end
553
716
  end
554
717
 
@@ -560,25 +723,29 @@ module Slinky
560
723
  # Builds the file by handling and compiling it and then copying it
561
724
  # to the build path
562
725
  def build
726
+ return nil unless should_build
727
+
563
728
  if !File.exists? @build_path
564
729
  FileUtils.mkdir_p(@build_path)
565
730
  end
566
731
  to = build_to
567
- begin
568
- path = process to
569
- rescue
570
- raise BuildFailedError
571
- end
732
+ path = process to
572
733
 
573
- if !path
574
- raise BuildFailedError
575
- elsif path != to
734
+ if path != to
576
735
  FileUtils.cp(path.to_s, to.to_s)
577
736
  @last_built = Time.now
578
737
  end
579
738
  to
580
739
  end
581
740
 
741
+ def should_build
742
+ @manifest.files_for_all_products.include?(self) || ![".js", ".css"].include?(output_path.extname)
743
+ end
744
+
745
+ def inspect
746
+ to_s
747
+ end
748
+
582
749
  def to_s
583
750
  "<Slinky::ManifestFile '#{@source}'>"
584
751
  end