slinky 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 1.9.2
data/Gemfile CHANGED
@@ -2,21 +2,22 @@ source "http://rubygems.org"
2
2
 
3
3
  gem "eventmachine", ">= 0.12.0"
4
4
  gem "eventmachine_httpserver", ">= 0.2.0"
5
- gem "em-proxy", ">= 0.1.5"
6
- gem "rainbow", ">= 1.1.1"
5
+ gem "em-proxy", ">= 0.1.6"
6
+ gem "rainbow", ">= 1.1.3"
7
7
  gem "haml", ">= 3.0.0"
8
8
  gem "sass", ">= 3.1.1"
9
9
  gem "coffee-script", ">= 2.2.0"
10
10
  gem "mime-types", ">= 1.16"
11
11
  gem "yui-compressor", ">= 0.9.6"
12
+ gem "listen", ">= 0.4.5"
12
13
 
13
14
  group :development do
14
- gem "rspec", "~> 2.3.0"
15
+ gem "rspec", "~> 2.10.0"
15
16
  gem "yard", "~> 0.6.0"
16
- gem "bundler", "~> 1.0.0"
17
- gem "jeweler", "~> 1.5.2"
17
+ gem "bundler", "~> 1.1.0"
18
+ gem "jeweler", "~> 1.8.0"
18
19
  gem 'cover_me', '>= 1.0.0.rc6'
19
- gem "fakefs"
20
- gem "em-http-request"
20
+ gem "fakefs", '~> 0.4.0'
21
+ gem "em-http-request", '~> 1.0.0'
21
22
  # gem "em-synchrony", ">= 0"
22
23
  end
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- #Slinky
1
+ #Slinky
2
2
 
3
3
  Slinky helps you write rich web applications using compiled web
4
4
  languages like SASS, HAML and CoffeeScript. The slinky server
@@ -11,6 +11,8 @@ Once you're ready for production the slinky builder will compile all of
11
11
  your sources and concatenate and minify your javascript and css,
12
12
  leaving you a directory that's ready to be pushed to your servers.
13
13
 
14
+ [![Build Status](https://secure.travis-ci.org/mwylde/slinky.png)](http://travis-ci.org/mwylde/slinky)
15
+
14
16
  ## Quickstart
15
17
 
16
18
  ```
@@ -82,7 +84,37 @@ a
82
84
  color: red
83
85
  ```
84
86
 
85
- ### Configuration
87
+ ### Specifing dependencies
88
+
89
+ As HAML and SASS scripts can include external content as part of their
90
+ build process, it may be that you would like to specify that files are
91
+ to be recompiled whenever other files change. For example, you may use
92
+ mustache templates defined each in their own file, but have set up
93
+ your HAML file to include them all into the HTML. Thus when one of the
94
+ mustache files changes, you would like the HAML file to be recompiled
95
+ so that the templates can be updated also.
96
+
97
+ These relationships are specified as "dependencies," and like requirements
98
+ they are incdicated through a special `slinky_depends("file")` directive in
99
+ your source files. For our template example, the index.haml files might look
100
+ like this:
101
+
102
+ ```haml
103
+ slinky_depends("scripts/templates/*.mustache")
104
+ !!!5
105
+
106
+ %html
107
+ %head
108
+ %title My App
109
+ slinky_styles
110
+ slinky_scripts
111
+ - Dir.glob("./scripts/templates/*.mustache") do |f|
112
+ - name = File.basename(f).split(".")[0..-2].join(".")
113
+ %script{:id => name, :type => "text/x-handlebars-template"}= File.read(f)
114
+ %body
115
+ ```
116
+
117
+ ## Configuration
86
118
 
87
119
  Slinky can optionally be configured using a yaml file. By default, it
88
120
  looks for a file called `slinky.yaml` in the source directory, but you
@@ -90,7 +122,7 @@ can also supply a file name on the command line using `-c`.
90
122
 
91
123
  There are currently two directives supported:
92
124
 
93
- #### Proxies
125
+ ### Proxies
94
126
 
95
127
  Slinky has a built-in proxy server which lets you test ajax requests
96
128
  with your actual backend servers. To set it up, your slinky.yaml file
@@ -128,7 +160,7 @@ an AJAX request is outstanding. However, when run locally the request
128
160
  returns so quickly that you can't even see the loading indicator. By
129
161
  adding in a lag this problem is remedied.
130
162
 
131
- #### Ignores
163
+ ### Ignores
132
164
 
133
165
  By default slinky will include every javascript and css file it finds
134
166
  into the combined scripts.js and styles.css files. However, it may be
data/Rakefile CHANGED
@@ -13,12 +13,12 @@ require 'jeweler'
13
13
  Jeweler::Tasks.new do |gem|
14
14
  # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
15
  gem.name = "slinky"
16
- gem.homepage = "http://github.com/mwylde/slinky"
16
+ gem.homepage = "http://mwylde.github.com/slinky/"
17
17
  gem.license = "MIT"
18
18
  gem.summary = %Q{Static file server for javascript apps}
19
- gem.description = %Q{A static file server for rich javascript apps that automatically compiles SASS, HAML, CoffeeScript and more}
20
- gem.email = "mwylde@wesleyan.edu"
21
- gem.authors = ["mwylde"]
19
+ gem.description = %Q{A static file server for rich web apps that automatically compiles SASS, HAML, CoffeeScript and more}
20
+ gem.email = "micah@micahw.com"
21
+ gem.authors = ["Micah Wylde"]
22
22
  end
23
23
  Jeweler::RubygemsDotOrgTasks.new
24
24
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.0
1
+ 0.6.0
data/lib/slinky.rb CHANGED
@@ -10,6 +10,7 @@ require 'rainbow'
10
10
  require 'optparse'
11
11
  require 'mime/types'
12
12
  require 'yui/compressor'
13
+ require 'listen'
13
14
 
14
15
  require "#{ROOT}/slinky/em-popen3"
15
16
  require "#{ROOT}/slinky/compilers"
@@ -20,6 +21,7 @@ require "#{ROOT}/slinky/proxy_server"
20
21
  require "#{ROOT}/slinky/server"
21
22
  require "#{ROOT}/slinky/runner"
22
23
  require "#{ROOT}/slinky/builder"
24
+ require "#{ROOT}/slinky/listener"
23
25
 
24
26
  # load compilers
25
27
  Dir.glob("#{ROOT}/slinky/compilers/*.rb").each{|compiler|
@@ -2,7 +2,11 @@ module Slinky
2
2
  class Builder
3
3
  def self.build dir, build_dir, config
4
4
  manifest = Manifest.new(dir, config, :build_to => build_dir, :devel => false)
5
- manifest.build
5
+ begin
6
+ manifest.build
7
+ rescue BuildFailedError
8
+ $stderr.puts "Build failed"
9
+ end
6
10
  end
7
11
  end
8
12
  end
@@ -80,11 +80,11 @@ module Slinky
80
80
  # Calls the supplied callback with the path of the compiled file,
81
81
  # compiling the source file first if necessary.
82
82
  def file &cb
83
- if needs_update?
84
- compile &cb
85
- else
86
- cb.call @output_path, nil, nil, nil
87
- end
83
+ #if needs_update?
84
+ compile &cb
85
+ #else
86
+ # cb.call @output_path, nil, nil, nil
87
+ #end
88
88
  end
89
89
 
90
90
  # Returns whether the source file has changed since it was last
@@ -0,0 +1,35 @@
1
+ Thread.abort_on_exception = true
2
+
3
+ module Slinky
4
+ class Listener
5
+ def initialize manifest
6
+ @manifest = manifest
7
+ end
8
+
9
+ def run
10
+
11
+ listener = Listen.to(@manifest.dir)
12
+ listener.change do |mod, add, rem|
13
+ handle_mod(mod) if mod.size > 0
14
+ handle_add(add) if add.size > 0
15
+ handle_rem(rem) if rem.size > 0
16
+ end
17
+ listener.start(false)
18
+ end
19
+
20
+ def handle_mod files
21
+ end
22
+
23
+ def handle_add files
24
+ EM.next_tick {
25
+ @manifest.add_all_by_path files
26
+ }
27
+ end
28
+
29
+ def handle_rem files
30
+ EM.next_tick {
31
+ @manifest.remove_all_by_path files
32
+ }
33
+ end
34
+ end
35
+ end
@@ -1,13 +1,18 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  require 'pathname'
3
+ require 'digest/md5'
3
4
 
4
5
  module Slinky
5
6
  # extensions of files that can contain build directives
6
7
  DIRECTIVE_FILES = %w{js css html haml sass scss coffee}
8
+ DEPENDS_DIRECTIVE = /^[^\n\w]*(slinky_depends)\((".*"|'.+'|)\)[^\n\w]*$/
7
9
  REQUIRE_DIRECTIVE = /^[^\n\w]*(slinky_require)\((".*"|'.+'|)\)[^\n\w]*$/
8
10
  SCRIPTS_DIRECTIVE = /^[^\n\w]*(slinky_scripts)[^\n\w]*$/
9
11
  STYLES_DIRECTIVE = /^[^\n\w]*(slinky_styles)[^\n\w]*$/
10
- BUILD_DIRECTIVES = Regexp.union(REQUIRE_DIRECTIVE, SCRIPTS_DIRECTIVE, STYLES_DIRECTIVE)
12
+ BUILD_DIRECTIVES = Regexp.union(DEPENDS_DIRECTIVE,
13
+ REQUIRE_DIRECTIVE,
14
+ SCRIPTS_DIRECTIVE,
15
+ STYLES_DIRECTIVE)
11
16
  CSS_URL_MATCHER = /url\(['"]?([^'"\/][^\s)]+\.[a-z]+)(\?\d+)?['"]?\)/
12
17
 
13
18
  # Raised when a compilation fails for any reason
@@ -46,7 +51,23 @@ module Slinky
46
51
  else
47
52
  @files.reject{|f| @config.ignores.any?{|p| f.in_tree? p}}
48
53
  end
49
- end
54
+ end
55
+
56
+ # Adds a file to the manifest, updating the dependency graph
57
+ def add_all_by_path paths
58
+ manifest_update paths do |path|
59
+ md = find_by_path(File.dirname(path)).first
60
+ mf = md.add_file(File.basename(path))
61
+ end
62
+ end
63
+
64
+ # Removes a file from the manifest
65
+ def remove_all_by_path paths
66
+ manifest_update paths do |path|
67
+ mf = find_by_path(path).first()
68
+ mf.parent.remove_file(mf)
69
+ end
70
+ end
50
71
 
51
72
  # Finds the file at the given path in the manifest if one exists,
52
73
  # otherwise nil.
@@ -54,8 +75,8 @@ module Slinky
54
75
  # @param String path the path of the file relative to the manifest
55
76
  #
56
77
  # @return ManifestFile the manifest file at that path if one exists
57
- def find_by_path path
58
- @manifest_dir.find_by_path path
78
+ def find_by_path path, allow_multiple = false
79
+ @manifest_dir.find_by_path path, allow_multiple
59
80
  end
60
81
 
61
82
  def scripts_string
@@ -71,19 +92,21 @@ module Slinky
71
92
  def compress ext, output, compressor
72
93
  scripts = dependency_list.reject{|x| x.output_path.extname != ext}
73
94
 
74
- s = scripts.collect{|s|
75
- f = File.open(s.build_to.to_s, 'rb'){|f| f.read}
76
- (block_given?) ? (yield s, f) : f
77
- }.join("\n")
95
+ if scripts.size > 0
96
+ s = scripts.collect{|s|
97
+ f = File.open(s.build_to.to_s, 'rb'){|f| f.read}
98
+ (block_given?) ? (yield s, f) : f
99
+ }.join("\n")
78
100
 
79
- File.open(output, "w+"){|f|
80
- f.write(compressor.compress(s))
81
- }
82
- scripts.collect{|s| FileUtils.rm(s.build_to)}
101
+ File.open(output, "w+"){|f|
102
+ f.write(compressor.compress(s))
103
+ }
104
+ scripts.collect{|s| FileUtils.rm(s.build_to)}
105
+ end
83
106
  end
84
107
 
85
108
  def compress_scripts
86
- compressor = YUI::JavaScriptCompressor.new(:munge => true)
109
+ compressor = YUI::JavaScriptCompressor.new(:munge => false)
87
110
  compress(".js", "#{@build_to}/scripts.js", compressor)
88
111
  end
89
112
 
@@ -118,9 +141,11 @@ module Slinky
118
141
  graph = []
119
142
  files(false).each{|mf|
120
143
  mf.directives[:slinky_require].each{|rf|
121
- required = mf.parent.find_by_path(rf)
122
- if required
123
- graph << [required, mf]
144
+ required = mf.parent.find_by_path(rf, true)
145
+ if required.size > 0
146
+ required.each{|x|
147
+ graph << [x, mf]
148
+ }
124
149
  else
125
150
  error = "Could not find file #{rf} required by #{mf.source}"
126
151
  $stderr.puts error.foreground(:red)
@@ -175,6 +200,27 @@ module Slinky
175
200
  files_rec c
176
201
  end
177
202
  end
203
+
204
+ def invalidate_cache
205
+ @files = nil
206
+ @dependency_graph = nil
207
+ end
208
+
209
+ def manifest_update paths
210
+ paths.each{|path|
211
+ if path[0] == '/'
212
+ path = Pathname.new(path).relative_path_from(Pathname.new(@dir).expand_path).to_s
213
+ end
214
+ yield path
215
+ }
216
+ invalidate_cache
217
+ files.each{|f|
218
+ if f.directives.include?(:slinky_scripts) || f.directives.include?(:slinky_styles)
219
+ f.invalidate
220
+ f.process
221
+ end
222
+ }
223
+ end
178
224
  end
179
225
 
180
226
  class ManifestDir
@@ -191,10 +237,9 @@ module Slinky
191
237
  # skip the build dir
192
238
  next if Pathname.new(File.absolute_path(path)) == Pathname.new(build_dir)
193
239
  if File.directory? path
194
- build_dir = (@build_dir + File.basename(path)).cleanpath
195
- @children << ManifestDir.new(path, self, build_dir, manifest)
240
+ add_child(path)
196
241
  else
197
- @files << ManifestFile.new(path, @build_dir, manifest, self)
242
+ add_file(path)
198
243
  end
199
244
  end
200
245
  end
@@ -203,15 +248,28 @@ module Slinky
203
248
  # otherwise nil.
204
249
  #
205
250
  # @param String path the path of the file relative to the directory
251
+ # @param Boolean allow_multiple if enabled, can return multiple paths
252
+ # according to glob rules
206
253
  #
207
- # @return ManifestFile the manifest file at that path if one exists
208
- def find_by_path path
254
+ # @return [ManifestFile] the manifest file at that path if one exists
255
+ def find_by_path path, allow_multiple = false
209
256
  components = path.to_s.split(File::SEPARATOR).reject{|x| x == ""}
210
257
  case components.size
211
258
  when 0
212
- self
259
+ [self]
213
260
  when 1
214
- @files.find{|f| f.matches? components[0]}
261
+ path = [@dir, components[0]].join(File::SEPARATOR)
262
+ if (Dir.exists?(path) rescue false)
263
+ c = @children.find{|d|
264
+ Pathname.new(d.dir).cleanpath == Pathname.new(path).cleanpath
265
+ }
266
+ unless c
267
+ c = add_child(path)
268
+ end
269
+ [c]
270
+ else
271
+ @files.find_all{|f| f.matches? components[0], allow_multiple}
272
+ end
215
273
  else
216
274
  if components[0] == ".."
217
275
  @parent.find_by_path components[1..-1].join(File::SEPARATOR)
@@ -219,24 +277,63 @@ module Slinky
219
277
  child = @children.find{|d|
220
278
  Pathname.new(d.dir).basename.to_s == components[0]
221
279
  }
222
- child ? child.find_by_path(components[1..-1].join(File::SEPARATOR)) : nil
280
+ if child
281
+ child.find_by_path(components[1..-1].join(File::SEPARATOR),
282
+ allow_multiple)
283
+ else
284
+ []
285
+ end
223
286
  end
224
287
  end
225
288
  end
226
289
 
290
+ # Adds a child directory
291
+ def add_child path
292
+ if Dir.exists? path
293
+ build_dir = (@build_dir + File.basename(path)).cleanpath
294
+ md = ManifestDir.new(path, self, build_dir, @manifest)
295
+ @children << md
296
+ md
297
+ end
298
+ end
299
+
300
+ # Adds a file on the filesystem to the manifest
301
+ #
302
+ # @param String path The path of the file
303
+ def add_file path
304
+ file = File.basename(path)
305
+ full_path = [@dir, file].join(File::SEPARATOR)
306
+ if File.exists? full_path
307
+ mf = ManifestFile.new(full_path, @build_dir, @manifest, self)
308
+ @files << mf
309
+ mf
310
+ end
311
+ end
312
+
313
+ # Removes a file from the manifest
314
+ #
315
+ # @param ManifestFile mf The file to be deleted
316
+ def remove_file mf
317
+ @files.delete(mf)
318
+ end
319
+
227
320
  def build
228
- if !@build_dir.exist?
229
- @build_dir.mkdir
321
+ unless Dir.exists?(@build_dir.to_s)
322
+ FileUtils.mkdir(@build_dir.to_s)
230
323
  end
231
324
  (@files + @children).each{|m|
232
325
  m.build
233
326
  }
234
327
  end
328
+
329
+ def to_s
330
+ "<ManifestDir:'#{@dir}'>"
331
+ end
235
332
  end
236
333
 
237
334
  class ManifestFile
238
335
  attr_accessor :source, :build_path
239
- attr_reader :last_built, :directives, :parent, :manifest
336
+ attr_reader :last_built, :directives, :parent, :manifest, :updated
240
337
 
241
338
  def initialize source, build_path, manifest, parent = nil, options = {:devel => false}
242
339
  @parent = parent
@@ -251,17 +348,32 @@ module Slinky
251
348
  @devel = true if options[:devel]
252
349
  end
253
350
 
351
+ def invalidate
352
+ @last_built = Time.new(0)
353
+ @last_md5 = nil
354
+ end
355
+
254
356
  # Predicate which determines whether the supplied name is the same
255
357
  # as the file's name, taking into account compiled file
256
358
  # extensions. For example, if mf refers to "/tmp/test/hello.sass",
257
359
  # `mf.matches? "hello.sass"` and `mf.matches? "hello.css"` should
258
360
  # both return true.
259
361
  #
260
- # @param [String] a filename
261
- # @return [Bool] True if the filename matches, false otherwise
262
- def matches? s
362
+ # @param String a filename
363
+ # @param Bool match_glob if true, matches according to glob rules
364
+ # @return Bool True if the filename matches, false otherwise
365
+ def matches? s, match_glob = false
263
366
  name = Pathname.new(@source).basename.to_s
264
- name == s || output_path.basename.to_s == s
367
+ output = output_path.basename.to_s
368
+ # check for stars that are not escaped
369
+ r = /(?<!\\)\*/
370
+ if match_glob && s.match(r)
371
+ a = s.split(r)
372
+ r2 = a.reduce{|a, x| /#{a}.*#{x}/}
373
+ name.match(r2) || output.match(r2)
374
+ else
375
+ name == s || output == s
376
+ end
265
377
  end
266
378
 
267
379
  # Predicate which determines whether the file is the supplied path
@@ -313,7 +425,7 @@ module Slinky
313
425
  end
314
426
  end
315
427
 
316
- directives
428
+ @directives = directives
317
429
  end
318
430
 
319
431
  # If there are any build directives for this file, the file is
@@ -323,19 +435,16 @@ module Slinky
323
435
  # @return String the path of the de-directivefied file
324
436
  def handle_directives path, to = nil
325
437
  if @directives.size > 0
326
- begin
327
- out = File.read(path)
328
- out.gsub!(REQUIRE_DIRECTIVE, "")
329
- out.gsub!(SCRIPTS_DIRECTIVE, @manifest.scripts_string)
330
- out.gsub!(STYLES_DIRECTIVE, @manifest.styles_string)
331
- to = to || Tempfile.new("slinky").path
332
- File.open(to, "w+"){|f|
333
- f.write(out)
334
- }
335
- to
336
- rescue
337
- nil
338
- end
438
+ out = File.read(path)
439
+ out.gsub!(DEPENDS_DIRECTIVE, "")
440
+ out.gsub!(REQUIRE_DIRECTIVE, "")
441
+ out.gsub!(SCRIPTS_DIRECTIVE, @manifest.scripts_string)
442
+ out.gsub!(STYLES_DIRECTIVE, @manifest.styles_string)
443
+ to = to || Tempfile.new("slinky").path + ".cache"
444
+ File.open(to, "w+"){|f|
445
+ f.write(out)
446
+ }
447
+ to
339
448
  else
340
449
  path
341
450
  end
@@ -357,14 +466,45 @@ module Slinky
357
466
  path ? Pathname.new(path) : nil
358
467
  end
359
468
 
469
+ # Gets the md5 hash of the source file
470
+ def md5
471
+ Digest::MD5.hexdigest(File.read(@source)) rescue nil
472
+ end
473
+
360
474
  # Gets manifest file ready for serving or building by handling the
361
475
  # directives and compiling the file if neccesary.
362
476
  # @param String path to which the file should be compiled
363
477
  #
364
478
  # @return String the path of the processed file, ready for serving
365
479
  def process to = nil
366
- # mangle file appropriately
367
- handle_directives (compile @source), to
480
+ return if @processing # prevent infinite recursion
481
+ start_time = Time.now
482
+ hash = md5
483
+ if hash != @last_md5
484
+ find_directives
485
+ end
486
+
487
+ depends = @directives[:slinky_depends].map{|f|
488
+ p = parent.find_by_path(f, true)
489
+ $stderr.puts "File #{f} depended on by #{@source} not found".foreground(:red) unless p.size > 0
490
+ p
491
+ }.flatten.compact if @directives[:slinky_depends]
492
+ depends ||= []
493
+ @processing = true
494
+ # process each file on which we're dependent, watching out for
495
+ # infinite loops
496
+ depends.each{|f| f.process }
497
+ @processing = false
498
+
499
+ # get hash of source file
500
+ if @last_path && hash == @last_md5 && depends.all?{|f| f.updated < start_time}
501
+ @last_path
502
+ else
503
+ @last_md5 = hash
504
+ @updated = Time.now
505
+ # mangle file appropriately
506
+ @last_path = handle_directives (compile @source), to
507
+ end
368
508
  end
369
509
 
370
510
  # Path to which the file will be built
@@ -379,8 +519,12 @@ module Slinky
379
519
  FileUtils.mkdir_p(@build_path)
380
520
  end
381
521
  to = build_to
382
- path = process to
383
-
522
+ begin
523
+ path = process to
524
+ rescue
525
+ raise BuildFailedError
526
+ end
527
+
384
528
  if !path
385
529
  raise BuildFailedError
386
530
  elsif path != to
@@ -389,5 +533,9 @@ module Slinky
389
533
  end
390
534
  to
391
535
  end
536
+
537
+ def to_s
538
+ "<Slinky::ManifestFile '#{@source}'>"
539
+ end
392
540
  end
393
541
  end