Slinky

If you write single-page rich client apps, Slinky is here to make your life easier. For development, it provides a static file server that transparently handles compiled languages like CoffeeScript and SASS while supporting advanced features like dependency management, proxying and automatic browser reloads. And once you're ready to deploy, Slinky will compile, concatenate, and minify your sources, leaving you ready to push to production.

Build Status

What can slinky do for you?

Slinky Server
  • Transparently compiles sources for a variety of languages
  • Supports the LiveReload protocol, for instant browser updates
  • Includes a customizable proxy, so your dev environment can mirror production
  • Includes support for HTML5 pushState based apps
Slinky Builder
  • Keeps track of the proper include order of your scripts and styles
  • Compiles, minifies and concatenates JavaScript and CSS

Slinky is not a framework, and it does not want to control your source code. Its goal is to help you when you want it—and get out of the way when you don't. It endeavors to be sufficiently flexible to support a wide variety of development styles.

Quick start

$ gem install slinky
$ cd ~/my/awesome/project/src
$ slinky start
[hardcore web development action]
$ slinky build -o ../pub
$ scp -r ../pub/ myserver.com:/var/www/project

The details

  1. Transparent compilation
  2. LiveReload/Guard support
  3. Script & style management
  4. Specifying order
  5. Dependencies
  6. Configuration
  7. PushState
  8. Proxies
  9. Ignores
  10. Products
  11. Path matching

Transparent compilation

The Slinky server will transparently compile various front-end languages for you, providing a smooth transition from development to production. What does this mean? When Slinky sees a request for a file that doesn't exist (say, "/scripts/core.js") it will look for a file that can be compiled into that ("/scripts/core.coffee"), compile it, then return the result. This allows you to write code without concern for which files are "native" and which need compilation.

Currently supported languages include:

  • CoffeeScript
  • HAML
  • SASS/SCSS
  • LESS
  • JSX (react templates)
  • ClojureScript (experimental)

Adding support for new languages is simple, and pull requests are welcome.

LiveReload/Guard support

The typical edit-save-reload cycle of web development can be tedious, especially when trying to get your CSS just right. What if you could reduce that to just edit-save? LiveReload allows just that. Slinky includes built-in support for LiveReload service. All you need to do is run a browser extension (available here for Safari, Chrome and Firefox) or include a little script (http://go.livereload.com/mobile). In addition to reloading your app whenever a source file changes, LiveReload supports hot reloading of CSS, letting you tweak your styles with ease. If don't want the LiveReload server running, disabling it is a simple --no-livereload away.

Script & style management

Slinky can manage all of your javascript and css files if you want it to, serving them up individually during development and concatenating and minifying them for production. To support this, Slinky recognizes slinky_scripts in your HTML/HAML files. For example, when Slinky sees this:

%html
  %head
    slinky_scripts
    slinky_styles
  %body
    %h1 Hello, world!

it will compile the HAML to HTML and replace slinky_styles with the appropriate HTML. You can also disable minification with the --dont-minify option or the dont_minify: true configuration option. slinky_scripts and slinky_styles are conveniences built on top of the full product system.

Specifying order

Often scripts and styles depend on being included in the page in a particular order. For this, we need the slinky_require directive.

For example, consider the case of two coffeescript files, A.coffee and B.coffee. A includes a class definition that B depends upon, so we want to make sure that A comes before B in the concatenation order.

File A.coffee:

class A
  hello: (thing) -> "Hello, " + thing

File B.coffee:

slinky_require("A.coffee")
alert (new A).hello("world")

We can also do this in CSS/SASS/SC SS:

/* slinky_require("reset.css")
a
  color: red

Dependencies

As HAML and SASS scripts can include external content as part of their build process, you may want certain files to be recompiled whenever other files change. For example, you may use mustache templates defined each in their own file, but have set up your HAML file to include them all into the HTML. Thus when one of the mustache files changes, you would like the HAML file to be recompiled so that the templates will also be updated.

These relationships are specified as "dependencies," and like requirements they are incdicated through a special slinky_depends("file") directive in your source files. For our template example, the index.haml files might look like this:

slinky_depends("scripts/templates/*.mustache")
!!!5

%html
  %head
    %title My App
    slinky_styles
    slinky_scripts
    - Dir.glob("./scripts/templates/*.mustache") do |f|
      - name = File.basename(f).split(".")[0..-2].join(".")
      %script{:id => name, :type => "text/x-handlebars-template"}= File.read(f)
  %body

The paths in slinky_depends can be either relative or absolute. If relative, they support simple globbing (as in the example above). If absolute, they support the full path matching syntax.

Configuration

Slinky can optionally be configured using a yaml file. By default, it looks for a file called slinky.yaml in the source directory, but you can also supply a file name on the command line using -c.

Most of what can be specified on the command line is also available in the configuration file. Here's a fully-decked out config:

pushstate:
  "/app1": "/index.html"
  "/app2": "/index2.html"
proxy:
  "/test1": "http://127.0.0.1:8000"
  "/test2": "http://127.0.0.1:7000"
produce:
  "/scripts.js":
    include:
      - "*.js"
    exclude:
      - "/script/vendor/"
      - "/script/jquery.js"
  "/styles.css":
    include:
      - "*.css"
port: 5555
src_dir: "src/"
build_dir: "build/"
no_proxy: true
no_livereload: true
livereload_port: 5556
dont_minify: true
# enable browser error injection (experimental)
enable_browser_errors: true

Most are self explanatory, but a few of the options merit further attention:

PushState

PushState is a new Javascript API that gives web apps more control over the browser's history, making possible single-page javascript applications that retain the advantages of their multi-page peers without resorting to hacks like hash urls. The essential idea is this: when a user navigates to a conceptually different "page" in the app, the URL should be updated to reflect that so that behaviors such as deep-linking and history navigation work properly.

For this to work, however, the server must be able to return the content of your main HTML page for arbitrary paths, as otherwise when a user tries to reload a pushstate-enabled web app they would receive a 404. Slinky supports multiple pushState paths using the pushstate configuration option:

pushstate:
  "/":     "/index.html"
  "/app1": "/app1/index.haml"
  "/app2": "/app2.haml"

Here, the key of the hash is a URL prefix, while the value is the file that should actually be displayed for non-existent requests that begin with the key. In the case of conflicting rules, the more specific one wins. For this config, instead of returning a 404 for a path like /this/file/does/not/exist, Slinky will send the content of /index.html, leaving your JavaScript free to render the proper view for that content. Similarly, a request for /app1/photo/1/edit, assuming such file does not exist, will return /app1/index.haml.

Proxies

Slinky has a built-in proxy server which lets you test ajax requests with your actual backend servers without violating the same-origin policy. To set it up, your slinky.yaml file will look something like this:

proxy:
  "/login": "http://127.0.0.1:4567/login"
  "/search":
    to: "http://127.0.0.1:4567/search"
    lag: 2000

What does this mean? We introduce the list of proxy rules using the proxy key. Each rule is a key value pair. The key is a url prefix to match against. The first rule is equivalent to the regular expression /\/login.*/, and will match paths like /login/user and /login/path/to/file.html. The value is either a url to pass the request on to or a hash containing configuration (one of which must be a to field). Currently a lag field is also supported. This delays the request by the specified number of milliseconds in order to simulate the latency associated with remote servers.

An example: we have some javascript code which makes an AJAX GET request to /search/widgets?q=foo. When Slinky gets the request it will see that it has a matching proxy rule, rewrites the request appropriately (changing paths and hosts) and sends it on to the backend server (in this case, 127.0.0.1:4567). Once it gets a response it will wait until 2 seconds has elapsed since slinky itself received the request and finally returns the response back to the browser.

Ignores

Ignores are deprecated and will be removed in the next major release. Use the new product system instead.

By default slinky will include every javascript and css file it finds into the combined scripts.js and styles.css files. However, it may be that for some reason you want to keep some files separate and handle them manually. The ignore directive lets you do that, by telling the system to skip over any files or directories listed. For example:

ignore:
  - script/vendor
  - css/reset.css

This will causes everything in the script/vendor directory to be ignored by slinky, as well as the reset.css file.

Products

New in 0.8: use master to get them now

Products are the outputs of the build system. Most files are just copied to the build directory, but you may want some to undergo further processing. For simplicity, Slinky defines two default products which you have seen above: /scripts.js and /styles.css. These are defined like this:

produce:
  "/scripts.js":
    include:
      - "*.js"
  "/styles.css":
    include:
      - "*.css"

Products are defined by an output path (in this case /scripts.js and /styles.css), a set of paths to include, and a set of paths to exclude (with gitignore-style glob patterns supported; see here for the match rules). In development mode, all of the files included in a product will be included in your html separately. When built in production mode, they will all be minified and concatenated into a single output file. We can also create our own products:

produce:
  "/test/test.js":
    include:
      - "*_test.js"
  "/main.js":
    include:
      - "*.js"
    exclude:
      - "vendor/jquery*.js"
      - "*_test.js"
  "/main.css":
    include:
      - "*.css"
    exclude:
      - "vendor/boostrap.css"

This config will produce three products in the build directory: test/test.js, which will include all files ending in _test.js, main.js which includes all .js files except jquery and test files, and main.css which includes all css files except for boostrap.css in the vendor directory. Custom products can be included in your HTML like this:

<html>
  <head>
    slinky_product("/main.js")
    slinky_product("/main.css")
  </head>
  ...

The default product directives (slinky_scripts and slinky_styles) are merely sugar for slinky_product("/scripts.js") and slinky_product("/styles.css").

Path matching

Several slinky config features involve specifying paths, with support for globbing. These are interpreted similarly to .gitignore rules. The full specification is:

  1. If the pattern ends with a slash, it will only match directories; e.g. foo/ would match a directory foo/ but not a file foo. In a file context, matching a directory is equivalent to matching all files under that directory, recursively.
  2. If the pattern does not contain a slash, slinky treats it as a relative pathname which can match files in any directory. For example, the rule test.js will matching /test.js and /component/test.js.
  3. If the pattern begins with a slash, it will be treated as an absolute path starting at the root of the source directory.
  4. If the pattern does not begin with a slash, but does contain one or more slashes, it will be treated as a path relative to any directory. For example, test/*.js will match /test/main.js, and /component/test/component.js, but not main.js.
  5. A single star * in a pattern will match any number of characters within a single path component. For example, /test/*.js will match /test/main_test.js but not /test/component/test.js.
  6. A double star ** will match any number of characters including path separators. For example /scripts/**/main.js will match any file named main.js under the /scripts directory, including /scripts/main.js and /scripts/component/main.js.