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.
- checksums.yaml +13 -5
- data/.travis.yml +1 -1
- data/Gemfile +2 -0
- data/README.md +119 -8
- data/VERSION +1 -1
- data/lib/slinky.rb +2 -0
- data/lib/slinky/builder.rb +4 -2
- data/lib/slinky/compiled_file.rb +1 -1
- data/lib/slinky/compilers.rb +5 -2
- data/lib/slinky/compilers/clojurescript-compiler.rb +4 -4
- data/lib/slinky/compilers/coffee-compiler.rb +4 -3
- data/lib/slinky/compilers/haml-compiler.rb +4 -3
- data/lib/slinky/compilers/jsx-compiler.rb +13 -0
- data/lib/slinky/compilers/less-compiler.rb +4 -3
- data/lib/slinky/compilers/sass-compiler.rb +4 -3
- data/lib/slinky/config_reader.rb +18 -2
- data/lib/slinky/errors.rb +91 -0
- data/lib/slinky/graph.rb +113 -0
- data/lib/slinky/manifest.rb +290 -123
- data/lib/slinky/proxy_server.rb +1 -1
- data/lib/slinky/runner.rb +1 -1
- data/lib/slinky/server.rb +41 -5
- data/lib/slinky/templates/error.css +32 -0
- data/lib/slinky/templates/error.haml +13 -0
- data/lib/slinky/templates/error.js +26 -0
- data/lib/slinky/templates/inject.css +27 -0
- data/slinky.gemspec +17 -3
- data/spec/compilers_spec.rb +15 -0
- data/spec/manifest_spec.rb +750 -0
- data/spec/slinky_spec.rb +9 -421
- data/spec/spec_helper.rb +22 -7
- metadata +83 -47
data/lib/slinky/graph.rb
ADDED
@@ -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
|
data/lib/slinky/manifest.rb
CHANGED
@@ -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
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
101
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
121
|
-
|
122
|
-
|
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
|
126
|
-
compressor =
|
203
|
+
def compress_product product
|
204
|
+
compressor = compressor_for_product product
|
205
|
+
post_processor = post_processor_for_product product
|
127
206
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
139
|
-
|
245
|
+
files_for_product(product).map{|f|
|
246
|
+
html_for_path("/#{f.relative_output_path}")
|
140
247
|
}.join("\n")
|
141
248
|
else
|
142
|
-
|
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
|
259
|
+
def dependency_graph
|
260
|
+
return @dependency_graph if @dependency_graph
|
261
|
+
|
153
262
|
graph = []
|
154
263
|
files(false).each{|mf|
|
155
|
-
mf.
|
156
|
-
|
157
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
204
|
-
|
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
|
-
|
355
|
-
|
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
|
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
|
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
|
486
|
-
out.gsub!(STYLES_DIRECTIVE
|
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
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
@
|
546
|
-
|
547
|
-
|
548
|
-
@updated
|
549
|
-
|
550
|
-
|
551
|
-
|
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
|
-
|
568
|
-
path = process to
|
569
|
-
rescue
|
570
|
-
raise BuildFailedError
|
571
|
-
end
|
732
|
+
path = process to
|
572
733
|
|
573
|
-
if
|
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
|