slinky 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,23 +1,25 @@
1
1
  source "http://rubygems.org"
2
2
 
3
+ # main gems
3
4
  gem "eventmachine", ">= 0.12.0"
4
5
  gem "eventmachine_httpserver", ">= 0.2.0"
6
+ gem "em-websocket", "~> 0.3.8"
5
7
  gem "em-proxy", ">= 0.1.6"
6
8
  gem "rainbow", ">= 1.1.3"
7
- gem "haml", ">= 3.0.0"
8
- gem "sass", ">= 3.1.1"
9
- gem "coffee-script", ">= 2.2.0"
10
9
  gem "mime-types", ">= 1.16"
11
10
  gem "yui-compressor", ">= 0.9.6"
12
11
  gem "listen", ">= 0.4.5"
13
12
 
13
+ # compilation support gems
14
+ gem "haml", ">= 3.0.0"
15
+ gem "sass", ">= 3.1.1"
16
+ gem "coffee-script", ">= 2.2.0"
17
+
14
18
  group :development do
15
19
  gem "rspec", "~> 2.10.0"
16
20
  gem "yard", "~> 0.6.0"
17
- gem "bundler", "~> 1.1.0"
21
+ gem "bundler", ">= 1.1.0"
18
22
  gem "jeweler", "~> 1.8.0"
19
- gem 'cover_me', '>= 1.0.0.rc6'
20
23
  gem "fakefs", '~> 0.4.0'
21
24
  gem "em-http-request", '~> 1.0.0'
22
- # gem "em-synchrony", ">= 0"
23
25
  end
data/README.md CHANGED
@@ -1,42 +1,79 @@
1
- #Slinky
1
+ # Slinky
2
2
 
3
- Slinky helps you write rich web applications using compiled web
4
- languages like SASS, HAML and CoffeeScript. The slinky server
5
- transparently compiles resources as they're requested, leaving you to
6
- worry about your code, not how to compile it. It will even proxy
7
- AJAX requests to a backend server so you can easily develop against
8
- REST APIs.
9
-
10
- Once you're ready for production the slinky builder will compile all of
11
- your sources and concatenate and minify your javascript and css,
12
- leaving you a directory that's ready to be pushed to your servers.
3
+ If you write single-page rich client apps, Slinky is here to
4
+ make your life easier. For development, it provides a static file
5
+ server that transparently handles compiled languages like CoffeeScript
6
+ and SASS while supporting advanced features like dependency management,
7
+ proxying and automatic browser reloads. And once you're ready to
8
+ deploy, Slinky will compile, concatenate, and minify your sources,
9
+ leaving you ready to push to production.
13
10
 
14
11
  [![Build Status](https://secure.travis-ci.org/mwylde/slinky.png)](http://travis-ci.org/mwylde/slinky)
15
12
 
16
- ## Quickstart
13
+ What can slinky do for you?
14
+
15
+ #### Slinky Server
16
+
17
+ * Transparently compiles sources for a variety of languages (currently
18
+ supported: CoffeeScript, ClojureScript, SASS/SCSS, LESS, HAML)
19
+ * Supports the [LiveReload](http://livereload.com) protocol, for
20
+ instant browser updates
21
+ * Allows proxying to backend servers, so you can develop your client
22
+ and server code separately
23
+ * Includes support for HTML5 [pushState](https://developer.mozilla.org/en-US/docs/DOM/Manipulating_the_browser_history) based apps
24
+
25
+ #### Slinky Builder
26
+
27
+ * Keeps track of the proper include order of your scripts and styles
28
+ * Compiles, minifies and concatenates JavaScript and CSS
29
+
30
+ Slinky is not a framework, and it does not want to control your source
31
+ code. Its goal is to help you when you want it—and get out of the way
32
+ when you don't. It endeavors to be sufficiently flexible to support a
33
+ wide variety of development styles.
34
+
35
+ ## Quick start
17
36
 
18
37
  ```
19
38
  $ gem install slinky
20
- $ cd ~/my/awesome/project
39
+ $ cd ~/my/awesome/project/src
21
40
  $ slinky start
22
41
  [hardcore web development action]
23
- $ slinky build
24
- $ scp -r build/ myserver.com/var/www/project
42
+ $ slinky build -o ../pub
43
+ $ scp -r ../pub/ myserver.com:/var/www/project
25
44
  ````
26
- ## But tell me more!
27
45
 
28
- Slinky currently supports three languages for compilation, SASS/SCSS,
29
- HAML and CoffeeScript, but it's simple to add support for others (and
30
- please submit a pull request when you do!). Slinky also has a few
31
- tricks of its own for managing the complexity of modern web
32
- development.
46
+ ## The details
47
+
48
+ 1. [LiveReload/Guard support](#livereloadguard-support)
49
+ 2. [Script & style management](#script--style-management)
50
+ 3. [Specifying order](#specifying-order)
51
+ 4. [Dependencies](#dependencies)
52
+ 5. [Configuration](#configuration)
53
+ 6. [PushState](#pushstate)
54
+ 7. [Proxies](#proxies)
55
+ 8. [Ignores](#ignores)
56
+
57
+ ### LiveReload/Guard support
58
+
59
+ The typical edit-save-reload cycle of web development can be tedious,
60
+ especially when trying to get your CSS *just* right. What if you could
61
+ reduce that to just edit-save? [LiveReload](http://livereload.com/)
62
+ allows just that. Slinky includes built-in support for LiveReload
63
+ service. All you need to do is run a browser extension (available
64
+ [here](http://go.livereload.com/extensions) for Safari, Chrome and
65
+ Firefox) or include a little script (http://go.livereload.com/mobile).
66
+ In addition to reloading your app whenever a source file changes,
67
+ LiveReload supports hot reloading of CSS, letting you tweak your
68
+ styles with ease. If don't want the LiveReload server running,
69
+ disabling it is a simple `--no-livereload` away.
33
70
 
34
71
  ### Script & style management
35
72
 
36
73
  Slinky can manage all of your javascript and css files if you want it
37
74
  to, serving them up individually during development and concatenating
38
75
  and minifying them for production. To support this, Slinky recognizes
39
- `slinky_scripts` in your HTML/Haml files. For example, when Slinky
76
+ `slinky_scripts` in your HTML/HAML files. For example, when Slinky
40
77
  sees this:
41
78
 
42
79
  ```haml
@@ -50,18 +87,19 @@ sees this:
50
87
  ```
51
88
 
52
89
  it will compile the HAML to HTML and replace slinky_styles with the
53
- appropriate HTML.
90
+ appropriate HTML. You can also disable minification with the
91
+ `--dont-minify` option or the `dont_minify: true` configuration
92
+ option.
54
93
 
55
94
  ### Specifying order
56
95
 
57
- But what if your scripts or styles depend on being included in the
58
- page in a particular order? For this, we need the `slinky_require`
96
+ Often scripts and styles depend on being included in the page
97
+ in a particular order. For this, we need the `slinky_require`
59
98
  directive.
60
99
 
61
100
  For example, consider the case of two coffeescript files, A.coffee and
62
101
  B.coffee. A includes a class definition that B depends upon, so we
63
- want to make sure that A comes before B in the concatenation order. We
64
- can solve this simply using `slinky_require(script)`
102
+ want to make sure that A comes before B in the concatenation order.
65
103
 
66
104
  File A.coffee:
67
105
 
@@ -76,7 +114,7 @@ File B.coffee:
76
114
  slinky_require("A.coffee")
77
115
  alert (new A).hello("world")
78
116
  ```
79
- We can also do this in CSS/SASS/SCSS:
117
+ We can also do this in CSS/SASS/SC SS:
80
118
 
81
119
  ```sass
82
120
  /* slinky_require("reset.css")
@@ -84,15 +122,15 @@ a
84
122
  color: red
85
123
  ```
86
124
 
87
- ### Specifing dependencies
125
+ ### Dependencies
88
126
 
89
127
  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.
128
+ build process, you may want certain files to be recompiled whenever
129
+ other files change. For example, you may use mustache templates
130
+ defined each in their own file, but have set up your HAML file to
131
+ include them all into the HTML. Thus when one of the mustache files
132
+ changes, you would like the HAML file to be recompiled so that the
133
+ templates will also be updated.
96
134
 
97
135
  These relationships are specified as "dependencies," and like requirements
98
136
  they are incdicated through a special `slinky_depends("file")` directive in
@@ -120,13 +158,70 @@ Slinky can optionally be configured using a yaml file. By default, it
120
158
  looks for a file called `slinky.yaml` in the source directory, but you
121
159
  can also supply a file name on the command line using `-c`.
122
160
 
123
- There are currently two directives supported:
161
+ Most of what can be specified on the command line is also available in
162
+ the configuration file. Here's a fully-decked out config:
163
+
164
+ ```yaml
165
+ pushstate:
166
+ "/app1": "/index.html"
167
+ "/app2": "/index2.html"
168
+ proxy:
169
+ "/test1": "http://127.0.0.1:8000"
170
+ "/test2": "http://127.0.0.1:7000"
171
+ ignore:
172
+ - script/vendor
173
+ - script/jquery.js
174
+ port: 5555
175
+ src_dir: "src/"
176
+ build_dir: "build/"
177
+ no_proxy: true
178
+ no_livereload: true
179
+ livereload_port: 5556
180
+ dont_minify: true
181
+ ```
182
+
183
+ Most are self explanatory, but a few of the options merit further
184
+ attention:
185
+
186
+ ### PushState
187
+
188
+ [PushState](https://developer.mozilla.org/en-US/docs/DOM/Manipulating_the_browser_history)
189
+ is a new Javascript API that gives web apps more control over the
190
+ browser's history, making possible single-page javascript applications
191
+ that retain the advantages of their multi-page peers without resorting
192
+ to hacks like hash urls. The essential idea is this: when a user
193
+ navigates to a conceptually different "page" in the app, the URL
194
+ should be updated to reflect that so that behaviors such as
195
+ deep-linking and history navigation work properly.
196
+
197
+ For this to work, however, the server must be able to return the
198
+ content of your main HTML page for arbitrary paths, as otherwise when
199
+ a user tries to reload a pushstate-enabled web app they would receive
200
+ a 404. Slinky supports multiple pushState paths using the pushstate
201
+ configuration option:
202
+
203
+ ```yaml
204
+ pushstate:
205
+ "/": "/index.html"
206
+ "/app1": "/app1/index.haml"
207
+ "/app2": "/app2.haml"
208
+ ```
209
+
210
+ Here, the key of the hash is a URL prefix, while the value is the file
211
+ that should actually be displayed for non-existent requests that begin
212
+ with the key. In the case of conflicting rules, the more specific one
213
+ wins. For this config, instead of returning a 404 for a path like
214
+ `/this/file/does/not/exist`, Slinky will send the content of
215
+ `/index.html`, leaving your JavaScript free to render the proper view for
216
+ that content. Similarly, a request for `/app1/photo/1/edit`, assuming
217
+ such file does not exist, will return `/app1/index.haml`.
124
218
 
125
219
  ### Proxies
126
220
 
127
221
  Slinky has a built-in proxy server which lets you test ajax requests
128
- with your actual backend servers. To set it up, your slinky.yaml file
129
- will look something like this:
222
+ with your actual backend servers without violating the same-origin
223
+ policy. To set it up, your slinky.yaml file will look something like
224
+ this:
130
225
 
131
226
  ```yaml
132
227
  proxy:
@@ -147,18 +242,12 @@ the request by the specified number of milliseconds in order to
147
242
  simulate the latency associated with remote servers.
148
243
 
149
244
  An example: we have some javascript code which makes an AJAX GET
150
- request to `/search/widgets?q=foo`. When slinky gets the request it
151
- will see that it has a matching proxy rule, rewrite the request
152
- appropriately (changing paths and hosts) and send it on to the backend
245
+ request to `/search/widgets?q=foo`. When Slinky gets the request it
246
+ will see that it has a matching proxy rule, rewrites the request
247
+ appropriately (changing paths and hosts) and sends it on to the backend
153
248
  server (in this case, 127.0.0.1:4567). Once it gets a response it will
154
249
  wait until 2 seconds has elapsed since slinky itself received the
155
- request and then send on the response back to the browser.
156
-
157
- This is very convenient for developing rich web clients locally. For
158
- example, you may have some code that shows a loading indicator while
159
- an AJAX request is outstanding. However, when run locally the request
160
- returns so quickly that you can't even see the loading indicator. By
161
- adding in a lag this problem is remedied.
250
+ request and finally returns the response back to the browser.
162
251
 
163
252
  ### Ignores
164
253
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.1
1
+ 0.7.0
data/bin/slinky CHANGED
@@ -1,8 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- root = File.expand_path(File.dirname(__FILE__))
4
-
5
- #require "#{root}/../lib/slinky"
6
3
  require 'slinky'
7
4
 
8
5
  Slinky::Runner.new(ARGV).run
@@ -1,7 +1,12 @@
1
1
  module Slinky
2
2
  class Builder
3
- def self.build dir, build_dir, config
4
- manifest = Manifest.new(dir, config, :build_to => build_dir, :devel => false)
3
+ def self.build options, config
4
+ dir = options[:src_dir] || config.src_dir
5
+ build_dir = options[:build_dir] || config.build_dir
6
+ manifest = Manifest.new(dir, config,
7
+ :build_to => build_dir,
8
+ :devel => false,
9
+ :no_minify => config.dont_minify || options[:no_minify])
5
10
  begin
6
11
  manifest.build
7
12
  rescue BuildFailedError
@@ -10,7 +10,7 @@ module Slinky
10
10
  def initialize source, compiler, output_ext
11
11
  @source = source
12
12
  @compiler = compiler
13
- @last_compiled = Time.new(0)
13
+ @last_compiled = Time.at(0)
14
14
  @output_ext = output_ext
15
15
  end
16
16
 
@@ -0,0 +1,18 @@
1
+ module Slinky
2
+ module ClojureScriptCompiler
3
+ Compilers.register_compiler self,
4
+ :inputs => ["cljs"],
5
+ :outputs => ["js"],
6
+ :dependencies => [["clementine", "~> 0.0.3"]]
7
+
8
+ def ClojureScriptCompiler::compile s, file
9
+ # Clementine.options[:pretty_print] = true
10
+ # Clementine.options[:optimizations] = :none
11
+ @engine ||= Clementine::ClojureScriptEngine.new(file,
12
+ :pretty_print => true,
13
+ :optimizations => :none,
14
+ :output_dir => Dir.tmpdir)
15
+ @engine.compile
16
+ end
17
+ end
18
+ end
@@ -1,10 +1,9 @@
1
- require 'coffee-script'
2
-
3
1
  module Slinky
4
2
  module CoffeeCompiler
5
3
  Compilers.register_compiler self,
6
4
  :inputs => ["coffee"],
7
- :outputs => ["js"]
5
+ :outputs => ["js"],
6
+ :dependencies => [["coffee-script", ">= 2.2.0"]]
8
7
 
9
8
  def CoffeeCompiler::compile s, file
10
9
  CoffeeScript::compile(s)
@@ -1,10 +1,9 @@
1
- require 'haml'
2
-
3
1
  module Slinky
4
2
  module HamlCompiler
5
3
  Compilers.register_compiler self,
6
- :inputs => ["haml"],
7
- :outputs => ["html"]
4
+ :inputs => ["haml"],
5
+ :outputs => ["html"],
6
+ :dependencies => [["haml", "~> 3.1.0"]]
8
7
 
9
8
  def HamlCompiler::compile s, file
10
9
  haml_engine = Haml::Engine.new(s)
@@ -0,0 +1,14 @@
1
+ module Slinky
2
+ module LessCompiler
3
+ Compilers.register_compiler self,
4
+ :inputs => ["less"],
5
+ :outputs => ["css"],
6
+ :dependencies => [["less", ">= 2.2.0"]]
7
+
8
+ def LessCompiler::compile s, file
9
+ parser = Less::Parser.new
10
+ tree = parser.parse(s)
11
+ tree.to_css
12
+ end
13
+ end
14
+ end
@@ -1,10 +1,9 @@
1
- require 'sass'
2
-
3
1
  module Slinky
4
2
  module SassCompiler
5
3
  Compilers.register_compiler self,
6
4
  :inputs => ["sass", "scss"],
7
- :outputs => ["css"]
5
+ :outputs => ["css"],
6
+ :dependencies => [["sass", ">= 3.1.1"]]
8
7
 
9
8
  def SassCompiler::compile s, file
10
9
  sass_engine = Sass::Engine.new(s, :load_paths => [File.dirname(file)])
@@ -1,3 +1,5 @@
1
+ require 'set'
2
+
1
3
  module Slinky
2
4
  EXTENSION_REGEX = /(.+)\.(\w+)/
3
5
 
@@ -5,6 +7,12 @@ module Slinky
5
7
  @compilers = []
6
8
  @compilers_by_ext = {}
7
9
  @compilers_by_input = {}
10
+ @checked_dependencies = Set.new
11
+
12
+ DEPENDENCY_NOT_MET = "Missing dependency %s (version %s), which is\n" +
13
+ "necessary to compile %s files\n\n" +
14
+ "Running the following should rectify this problem:\n" +
15
+ " $ gem install --version '%s' %s"
8
16
 
9
17
  class << self
10
18
  def register_compiler klass, options
@@ -19,6 +27,30 @@ module Slinky
19
27
  end
20
28
  end
21
29
 
30
+ def get_cfile source, compiler, input_ext, output_ext
31
+ if has_dependencies compiler, input_ext
32
+ CompiledFile.new source, compiler[:klass], output_ext
33
+ end
34
+ end
35
+
36
+ def has_dependencies compiler, ext
37
+ (compiler[:dependencies] || []).all? {|d|
38
+ if @checked_dependencies.include?(d)
39
+ true
40
+ else
41
+ begin
42
+ gem(d[0], d[1])
43
+ require d[0]
44
+ @checked_dependencies.add(d)
45
+ true
46
+ rescue Gem::LoadError
47
+ $stderr.puts((DEPENDENCY_NOT_MET % [d[0], d[1], ext, d[1], d[0]]).foreground(:red))
48
+ false
49
+ end
50
+ end
51
+ }
52
+ end
53
+
22
54
  # Produces a CompiledFile for an input file if the file needs to
23
55
  # be compiled, or nil otherwise. Note that path is the path of
24
56
  # the compiled file, so script.js not script.coffee.
@@ -44,7 +76,7 @@ module Slinky
44
76
  compilers[extension].each do |c|
45
77
  c[:inputs].each do |i|
46
78
  if files_by_ext[i]
47
- cfile = CompiledFile.new files_by_ext[i], c[:klass], extension
79
+ cfile = get_cfile(files_by_ext[i], c, ext, extension)
48
80
  break
49
81
  end
50
82
  end
@@ -62,7 +94,7 @@ module Slinky
62
94
  _, file, ext = path.match(EXTENSION_REGEX).to_a
63
95
 
64
96
  if ext && ext != "" && compiler = @compilers_by_input[ext]
65
- CompiledFile.new path, compiler[:klass], compiler[:outputs].first
97
+ get_cfile(path, compiler, ext, compiler[:outputs].first)
66
98
  else
67
99
  nil
68
100
  end
@@ -19,5 +19,54 @@ module Slinky
19
19
  def ignores
20
20
  @config["ignore"] || []
21
21
  end
22
+
23
+ def port
24
+ @config["port"] || 5323
25
+ end
26
+
27
+ def src_dir
28
+ @config["src_dir"] || "."
29
+ end
30
+
31
+ def build_dir
32
+ @config["build_dir"] || "build"
33
+ end
34
+
35
+ def no_proxy
36
+ @config["no_proxy"] || false
37
+ end
38
+
39
+ def no_livereload
40
+ @config["no_livereload"] || false
41
+ end
42
+
43
+ def livereload_port
44
+ @config["livereload_port"] || 35729
45
+ end
46
+
47
+ def dont_minify
48
+ @config["dont_minify"] || false
49
+ end
50
+
51
+ def pushstates
52
+ @config["pushstate"]
53
+ end
54
+
55
+ def pushstate_for_path path
56
+ if pushstates && pushstates.is_a?(Hash)
57
+ p = pushstates.sort_by{|from, to| -from.count("/")}.find{|a|
58
+ path.start_with? a[0]
59
+ }
60
+ p[1] if p
61
+ end
62
+ end
63
+
64
+ def [](x)
65
+ @config[x]
66
+ end
67
+
68
+ def to_s
69
+ @config.to_s
70
+ end
22
71
  end
23
72
  end
@@ -2,17 +2,32 @@ Thread.abort_on_exception = true
2
2
 
3
3
  module Slinky
4
4
  class Listener
5
- def initialize manifest
5
+ def initialize manifest, livereload
6
6
  @manifest = manifest
7
+ @livereload = livereload
7
8
  end
8
9
 
9
10
  def run
10
-
11
11
  listener = Listen.to(@manifest.dir)
12
12
  listener.change do |mod, add, rem|
13
13
  handle_mod(mod) if mod.size > 0
14
14
  handle_add(add) if add.size > 0
15
- handle_rem(rem) if rem.size > 0
15
+
16
+ EM.next_tick {
17
+ files = (mod + add + rem).map{|path|
18
+ mpath = Pathname.new(path)\
19
+ .relative_path_from(Pathname.new(@manifest.dir).expand_path).to_s
20
+ mf = @manifest.find_by_path(mpath, false).first
21
+ if mf
22
+ mf.output_path
23
+ else
24
+ path
25
+ end
26
+ }
27
+ @livereload.reload_browser(files)
28
+ } if @livereload
29
+
30
+ handle_rem(rem) if rem.size > 0
16
31
  end
17
32
  listener.start(false)
18
33
  end
@@ -22,13 +37,19 @@ module Slinky
22
37
 
23
38
  def handle_add files
24
39
  EM.next_tick {
25
- @manifest.add_all_by_path files
40
+ begin
41
+ @manifest.add_all_by_path files
42
+ rescue
43
+ end
26
44
  }
27
45
  end
28
-
46
+
29
47
  def handle_rem files
30
48
  EM.next_tick {
31
- @manifest.remove_all_by_path files
49
+ begin
50
+ @manifest.remove_all_by_path files
51
+ rescue
52
+ end
32
53
  }
33
54
  end
34
55
  end
@@ -0,0 +1,51 @@
1
+ module Slinky
2
+ # Code to interface with the LiveReload set of browser plugins and
3
+ # javascript clients. Adapted from the guard-livereload project at
4
+ # github.com/guard/guard-livereload.
5
+ class LiveReload
6
+ def initialize host, port
7
+ @host = host
8
+ @port = port
9
+ @websockets = []
10
+ end
11
+
12
+ def run
13
+ EventMachine.run do
14
+ begin
15
+ EventMachine.start_server(@host,
16
+ @port,
17
+ EventMachine::WebSocket::Connection, {}) do |ws|
18
+ ws.onopen do
19
+ begin
20
+ $stdout.puts "Browser connected to livereload server"
21
+ ws.send "!!ver:1.6"
22
+ @websockets << ws
23
+ rescue
24
+ $stderr.puts $!.to_s.foreground(:red)
25
+ end
26
+ end
27
+
28
+ ws.onclose do
29
+ @websockets.delete ws
30
+ $stdout.puts "Browser disconnected"
31
+ end
32
+ end
33
+ $stdout.puts "Started live-reload server on port #{@port}"
34
+ rescue
35
+ puts "Unable to start livereload server on port #{@port}".foreground(:red)
36
+ end
37
+ end
38
+ end
39
+
40
+ def reload_browser(paths = [])
41
+ paths.each do |path|
42
+ data = MultiJson.encode(['refresh', {
43
+ :path => "#{path}",
44
+ :apply_js_live => false,
45
+ :apply_css_live => true
46
+ }])
47
+ @websockets.each { |ws| ws.send(data) }
48
+ end
49
+ end
50
+ end
51
+ end