slinky 0.7.3 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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