Skip to content

Instantly share code, notes, and snippets.

@mislav
Created July 21, 2010 18:25
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save mislav/484894 to your computer and use it in GitHub Desktop.
Save mislav/484894 to your computer and use it in GitHub Desktop.
"livereload" server replacement for working with Rails

A replacement for "livereload" gem on OS X

This script is a replacement for livereload server component designed for working with Rails. It watches the filesystem with FS Events (Mac OS X) rather than with EventMachine. This is better for large projects for wich EventMachine fails with "too many open files" exception.

Sass is supported; .sass files can also be stored in "app/styles/" directory. Compass is detected if "config/compass.rb" file exists.

Installation:

Download this script to somewhere in your PATH and make it executable. You can name it something different than "livereload" if you want to try this and the official gem executable in parallel.

Then, visit the official livereload project for instructions on how to install the browser component.

Details

  1. .erb and .haml templates under "app/": page reload
  2. .rb files under "app/helpers/": page reload
  3. .html and .js files under "public/": page reload
  4. .css files under "public/": live style update!
  5. .sass and .scss files anywhere: compile to CSS

Command line options are "-D" to enable debug mode. All other parameters (if given) specify a list of directories to watch. This is not compatible with livereload gem, which supports many more options and customization. If you want customization, edit this script. It's simple.

#!/usr/bin/ruby -rubygems
# Replacement for "livereload" gem for working with Rails
#
# Dependencies:
# $ sudo /usr/bin/gem install mislav-rspactor em-websocket json haml
# uncomment to force loading the correct rspactor version
# gem 'mislav-rspactor', '~> 0.4.0'
require 'rspactor'
require 'em-websocket'
require 'json'
require 'sass/plugin'
require 'fileutils'
API_VERSION = '1.3'
web_sockets = []
debug = !!ARGV.delete('-D')
dirs = ARGV.empty?? [Dir.pwd] : ARGV
# Compass support
compass_config = "./config/compass.rb"
if File.exists? compass_config
# $ sudo /usr/bin/gem install compass
require 'compass'
Compass.add_project_configuration(compass_config)
Compass.configure_sass_plugin!
Compass.handle_configuration_change!
else
Sass::Plugin.add_template_location(Dir.pwd + '/app/styles')
end
# Jekyll support
jekyll_mode = %w[_config.yml _site].any? { |file| File.exists? file }
if jekyll_mode
require 'jekyll'
require 'webrick'
options = Jekyll.configuration({})
# Get source and destination directories (possibly set by config file)
source = options['source']
destination = options['destination']
# Create the Site
jekyll_site = Jekyll::Site.new(options)
FileUtils.mkdir_p(destination)
mime_types = WEBrick::HTTPUtils::DefaultMimeTypes
mime_types.store 'js', 'application/javascript'
jekyll_server = WEBrick::HTTPServer.new(
:Port => options['server_port'],
:DocumentRoot => destination,
:MimeTypes => mime_types
)
end
sinatra_mode = File.exists? 'app.rb'
extensions = %w[html erb haml sass scss css js rb yml]
extensions.concat %w[md markdown textile xml] if jekyll_mode
extensions << 'mustache' if sinatra_mode
ws_push = lambda do |file|
data = ['refresh', { :path => file, :apply_js_live => false, :apply_css_live => true }].to_json
puts data if debug
# send it to the browser!
web_sockets.each { |ws| ws.send(data) }
end
process_file = if sinatra_mode
lambda do |file|
case file
when %r{/(views|templates)/}, %r{/public/.+\.(css|js|html)$}
file = file.sub(%r{\.s[ac]ss$}, '.css')
ws_push[file]
true
else
false
end
end
else
lambda do |file|
case file
when %r{/app/.+\.(erb|haml)$}, %r{/app/helpers/.+\.rb$}, # application view code
%r{/public/.+\.(css|js|html)$}, # static assets
%r{/config/locales/.+\.yml$}, # translation files
%r{/_site/.+\.css$} # styles in jekyll
ws_push[file]
true
when %r{\.s[ac]ss$}
puts "Regenerating CSS stylesheets..." if debug
Sass::Plugin.update_stylesheets
true
else
false
end
end
end
listener = RSpactor::Listener.new(:extensions => extensions, :relative_paths => false) { |files|
for file in files
done = process_file[file]
puts "Unhandled change: #{file}" if not done and debug and not jekyll_mode
end
if jekyll_mode and not files.any? { |file| file.index(destination) == 0 }
# puts "Regenerating site..."
# jekyll_site.process
files.select { |file| file =~ /\.css/ }.each do |file|
path = file.sub(Dir.pwd + '/', '')
target_file = File.expand_path(path, destination)
listener.force_changed << target_file
FileUtils.cp file, target_file
end
end
}
Sass::Plugin.on_updating_stylesheet do |template, css|
# We notify the listener that the stylesheet file changed for sure.
# Because the filesystem can't know modification time in milliseconds,
# this prevents FSEvents handler from thinking the file didn't change.
stylesheet_file = File.expand_path(css, Dir.pwd)
puts "generating #{stylesheet_file}" if debug
listener.force_changed << stylesheet_file
end
Sass::Plugin.on_compilation_error do |e, template, css|
warn "error compiling #{template}"
end
puts "Starting file watcher for directories: #{dirs.inspect}"
listener.start(dirs)
# Disable the RubyCocoa thread hook as apparently Laurent did not apply the
# thread patches to the OS X system Ruby
ENV['RUBYCOCOA_THREAD_HOOK_DISABLE'] = 'kampai'
Thread.new { OSX.CFRunLoopRun }
if jekyll_mode
Thread.new { jekyll_server.start }
trap("INT") { jekyll_server.shutdown }
end
trap("INT") { EventMachine.stop }
EventMachine.run do
puts "LiveReload is waiting for a browser to connect."
EventMachine::WebSocket.start(:host => '0.0.0.0', :port => '10083', :debug => debug) do |ws|
ws.onopen do
begin
puts "Browser connected."
ws.send "!!ver:#{API_VERSION}"
web_sockets << ws
rescue
puts $!
puts $!.backtrace
end
end
ws.onmessage do |msg|
puts "Browser URL: #{msg}"
end
ws.onclose do
web_sockets.delete ws
puts "Browser disconnected."
end
end
end
@will
Copy link

will commented Jul 21, 2010

What version of rspactor are you using? I'm on 0.6.4 and get

livereload:65: undefined method start' for #<RSpactor::Listener:0x1011887b8> (NoMethodError) from /Users/will/.rvm/gems/ree-1.8.7-2010.02/gems/eventmachine-0.12.10/lib/eventmachine.rb:256:incall'
from /Users/will/.rvm/gems/ree-1.8.7-2010.02/gems/eventmachine-0.12.10/lib/eventmachine.rb:256:in run_machine' from /Users/will/.rvm/gems/ree-1.8.7-2010.02/gems/eventmachine-0.12.10/lib/eventmachine.rb:256:inrun'
from script/livereload:40

@mislav
Copy link
Author

mislav commented Jul 21, 2010

No, I'm using my fork which I published as "mislav-rspactor" gem. I've indicated this in the instructions. I've updated the comments with a gem command you can use to explicitly state which gem you want to use.

@thibaudgg
Copy link

Great, I'll add this to RSpactor 0.7! Do you thing debug option is needed?

@thibaudgg
Copy link

Oh! btw no more rubycocoa dependency with RSpactor 0.7 ;-)

@mislav
Copy link
Author

mislav commented Jul 21, 2010

I know, I've been following. But wasn't it reverted?

About this in rspactor: I don't think it's rspactor material. I'd rather publish it as a fork of livereload ('cause that's what it is)

@mislav
Copy link
Author

mislav commented Jul 21, 2010

Ah, I see now what you meant about fsevent_watch. Interesting stuff. I'll try 0.7, maybe even merge some of my own ideas into it. But for now, I'm sticking to my forks of both livereload and rspactor. When my livereload gets more solid (meaning tested against real world projects) we can think about having it in rspactor.

@will
Copy link

will commented Jul 21, 2010

having this livereload stuff in rspactor itself would be amazing

@andreyvit
Copy link

But this:

Thread.new { OSX.CFRunLoopRun }

requires Ruby 1.9, right?

@mislav
Copy link
Author

mislav commented Jul 22, 2010

Nope. Because this requires system ruby on OS X, it can obviously only run on 1.8.7 which is installed with Snow Leopard. Might work 1.8.6 in Leopard too.

@cehoffman
Copy link

Trying to understand the reason for this exactly.

Thread.new { OSX.CFRunLoopRun }

Is this for rspactor before 0.7 when RubyCocoa was required?

@mislav
Copy link
Author

mislav commented Jul 28, 2010

Yes, this code depends on my fork of rspactor which is still bound to RubyCocoa. Notice that I used "/usr/bin/ruby" in installation instructions and in the shebang.

My fork of rspactor is soon going independent of RubyCocoa, and it will even run on Linux. I cherry picked this functionality from the official fork and improved it, but I haven't tested and released yet.

@mislav
Copy link
Author

mislav commented Jul 29, 2010

Just updated with extra mappings and experimental Compass support.

@will
Copy link

will commented Jul 29, 2010

Compass support works great. I had to add in my own require for fancy-buttons since that was in my compass initializer. But other than that, it's working perfectly.

@andreyvit
Copy link

Info for the guys (and girls) using this alternate tool: LiveReload 1.4 should now work with large projects, check it out. Also you are encouraged to open issues on github.com/mockko/livereload/issues for any functionality you miss in the official LiveReload gem.

@mislav, we're very much interested in allowing integration of LiveReload with other tools without just copying/rewriting the server code. Please let us know what we can do to enable that.

Also,

Thread.new { OSX.CFRunLoopRun }

gives a warning from Ruby 1.8.7 that you can't use threads in RubyCocoa. Depending on some random condition the application either crashes or continues working after that. Our em-dir-watcher gem forks a subprocess instead.

[UPDATE] OK, I see this code — thanks a lot for the pointer, maybe we'll use it:

# Disable the RubyCocoa thread hook as apparently Laurent did not apply the
# thread patches to the OS X system Ruby
ENV['RUBYCOCOA_THREAD_HOOK_DISABLE'] = 'kampai'

@thibaudgg
Copy link

@andreyvit Have you look how RSpactor (http://github.com/thibaudgg/rspactor) handle FSEvents (without RubyCocoa & EM) ? Feel free to contact me if you have any questions.

@rizwanreza
Copy link

Thanks mislav, works like a charm. How about wrapping it up into a gem, or possibly merging it with livereload?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment