spar 0.2.7 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/README.md +272 -1
  2. data/bin/spar +30 -1
  3. data/lib/spar.rb +128 -7
  4. data/lib/spar/assets/404.jpg +0 -0
  5. data/lib/spar/assets/500.jpg +0 -0
  6. data/lib/spar/cli.rb +75 -22
  7. data/lib/spar/compiled_asset.rb +90 -0
  8. data/lib/spar/compiler.rb +74 -0
  9. data/lib/spar/compressor.rb +18 -0
  10. data/lib/spar/deployers/cloudfront_deployer.rb +33 -0
  11. data/lib/spar/deployers/deployer.rb +26 -0
  12. data/lib/spar/deployers/local_deployer.rb +15 -0
  13. data/lib/spar/deployers/s3_deployer.rb +75 -0
  14. data/lib/spar/directive_processor.rb +36 -0
  15. data/lib/spar/exceptions.rb +330 -0
  16. data/lib/spar/generators/templates/{README → base/README} +0 -0
  17. data/lib/spar/generators/templates/base/config.yml +12 -0
  18. data/lib/spar/generators/templates/pages/haml/app/pages/index.html.haml +11 -0
  19. data/lib/spar/generators/templates/pages/html/app/pages/index.html +16 -0
  20. data/lib/spar/generators/templates/{app/assets/javascripts/javascript.js.coffee → scripts/coffee/app/javascripts/application.js.coffee} +6 -3
  21. data/lib/spar/generators/templates/scripts/js/app/javascripts/application.js +11 -0
  22. data/lib/spar/generators/templates/styles/css/app/stylesheets/application.css +18 -0
  23. data/lib/spar/generators/templates/{app/assets → styles/sass/app}/stylesheets/application.css.sass +8 -4
  24. data/lib/spar/helpers.rb +31 -102
  25. data/lib/spar/not_found.rb +64 -0
  26. data/lib/spar/rewrite.rb +29 -0
  27. data/lib/spar/static.rb +46 -0
  28. data/lib/spar/tasks.rb +8 -13
  29. data/lib/spar/version.rb +1 -1
  30. metadata +261 -66
  31. data/lib/spar/assets.rb +0 -78
  32. data/lib/spar/base.rb +0 -54
  33. data/lib/spar/css_compressor.rb +0 -17
  34. data/lib/spar/deployer.rb +0 -198
  35. data/lib/spar/generators/application.rb +0 -42
  36. data/lib/spar/generators/templates/Gemfile +0 -3
  37. data/lib/spar/generators/templates/Rakefile +0 -8
  38. data/lib/spar/generators/templates/app/views/index.haml +0 -7
  39. data/lib/spar/generators/templates/config.ru +0 -9
  40. data/lib/spar/generators/templates/config/application.rb +0 -12
  41. data/lib/spar/generators/templates/script/server +0 -1
  42. data/lib/spar/static_compiler.rb +0 -91
Binary file
Binary file
data/lib/spar/cli.rb CHANGED
@@ -1,32 +1,85 @@
1
- require 'rbconfig'
1
+ require 'pathname'
2
+ require 'fileutils'
3
+ require 'listen'
4
+ require 'rack'
5
+ require 'thor'
2
6
 
3
- if RUBY_VERSION < '1.9.2'
4
- desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})"
5
- abort <<-end_message
7
+ module Spar
8
+ class CLI < Thor
9
+ include Thor::Actions
6
10
 
7
- Spar requires Ruby 1.9.2+.
11
+ def self.source_root
12
+ File.expand_path('../../..', __FILE__)
13
+ end
8
14
 
9
- You're running
10
- #{desc}
15
+ desc 'server', 'Serve spar application'
11
16
 
12
- Please upgrade to continue.
17
+ method_option :port, :aliases => '-p', :desc => 'Port'
13
18
 
14
- end_message
15
- end
16
- Signal.trap("INT") { puts; exit(1) }
19
+ def server
20
+ Rack::Server.start(
21
+ :Port => options[:port] || 8888,
22
+ :app => Spar.app
23
+ )
24
+ end
17
25
 
18
- require 'spar/version'
26
+ desc 'deploy', 'Deploy the project.'
19
27
 
20
- if ['--version', '-v'].include?(ARGV.first)
21
- puts "Spar #{Spar::VERSION}"
22
- exit(0)
23
- end
28
+ def deploy(environment='production')
29
+ say "Deploying: #{Spar.root}"
24
30
 
25
- if ARGV.first.nil?
26
- puts "Please specify a name for your new app like so: 'spar APP_NAME'"
27
- exit(0)
28
- end
31
+ Spar.environment = environment
32
+
33
+ if strategy = Spar.settings['deploy_strategy']
34
+ require "spar/deployers/#{strategy}_deployer"
35
+
36
+ deployer = Kernel.const_get("#{strategy.capitalize()}Deployer").new
29
37
 
30
- require 'spar/generators/application'
38
+ deployer.run(Spar::Compiler.assets)
39
+ else
40
+ raise "You are trying to deploy to the #{environment} environment, but you have not declared a :deploy_strategy in config.yml for this environment. The available options are: 'local', 's3', and 'cloudfront'."
41
+ end
42
+ end
31
43
 
32
- Spar::Generators::Application.new.generate(ARGV.first)
44
+ desc 'version', 'Show the current version of Spar'
45
+
46
+ def version
47
+ puts "Spar Version #{Spar::VERSION}"
48
+ end
49
+
50
+ desc 'new', 'Create a new spar project'
51
+
52
+ method_option :css, :desc => 'Use css instead of sass'
53
+ method_option :html, :desc => 'Use html instead of haml'
54
+ method_option :js, :desc => 'Use js instead of coffeescript'
55
+
56
+ def new(name)
57
+ # first, copy in the base directory
58
+ directory('lib/spar/generators/templates/base', name)
59
+
60
+ if !options[:css]
61
+ # copy in the sass files
62
+ directory('lib/spar/generators/templates/styles/sass', name)
63
+ else
64
+ # copy in the css files
65
+ directory('lib/spar/generators/templates/styles/css', name)
66
+ end
67
+
68
+ if !options[:html]
69
+ # copy in the haml files
70
+ directory('lib/spar/generators/templates/pages/haml', name)
71
+ else
72
+ # copy in the html files
73
+ directory('lib/spar/generators/templates/pages/html', name)
74
+ end
75
+
76
+ if !options[:js]
77
+ # copy in the coffee files
78
+ directory('lib/spar/generators/templates/scripts/coffee', name)
79
+ else
80
+ # copy in the js files
81
+ directory('lib/spar/generators/templates/scripts/js', name)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,90 @@
1
+ require 'mime/types'
2
+
3
+ module Spar
4
+
5
+ class CompiledAsset
6
+
7
+ attr_accessor :asset, :logical_path, :write_path, :headers, :mtime, :digest
8
+
9
+ def initialize(asset, options={})
10
+ if asset.is_a?(String)
11
+ @file = File.new(asset, 'rb')
12
+ @logical_path = asset
13
+ else
14
+ @asset = asset
15
+ @logical_path = @asset.logical_path
16
+ end
17
+ @write_path = generate_write_path(options)
18
+ @headers = generate_headers(options)
19
+ @mtime = (@asset || @file).mtime
20
+ @digest = @asset ? @asset.digest : Digest::MD5.hexdigest(data)
21
+ end
22
+
23
+ def generate_write_path(options)
24
+ if options[:digest] && @asset
25
+ @asset.digest_path
26
+ else
27
+ @asset ? @logical_path : @logical_path.gsub(/^static\//,'')
28
+ end
29
+ end
30
+
31
+ def generate_headers(options)
32
+ headers = {
33
+ :cache_control => options[:cache_control] || Spar.settings['cache_control'],
34
+ :acl => :public_read
35
+ }
36
+
37
+ if @write_path =~ /\/.html$/
38
+ headers[:content_type] = 'text/html; charset=utf-8'
39
+ else
40
+ headers[:content_type] = MIME::Types.of(@write_path).first
41
+ end
42
+
43
+ if @logical_path =~ /static\/downloads\//
44
+ headers[:content_disposition] = "attachment; filename=#{File.basename(@write_path)}"
45
+ end
46
+
47
+ headers[:content_encoding] = :gzip if %w[svgz gz].index @write_path.split('.').last
48
+
49
+ headers
50
+ end
51
+
52
+ def data
53
+ @data ||= @asset ? @asset.to_s : @file.read
54
+ end
55
+
56
+ def write_to(base_path, options = {})
57
+ filename = File.join(base_path, @write_path)
58
+ # Gzip contents if filename has '.gz'
59
+ options[:compress] ||= File.extname(filename) == '.gz'
60
+
61
+ FileUtils.mkdir_p File.dirname(filename)
62
+
63
+ File.open("#{filename}+", 'wb') do |f|
64
+ if options[:compress]
65
+ # Run contents through `Zlib`
66
+ gz = Zlib::GzipWriter.new(f, Zlib::BEST_COMPRESSION)
67
+ gz.mtime = @mtime.to_i
68
+ gz.write data
69
+ gz.close
70
+ else
71
+ # Write out as is
72
+ f.write data
73
+ f.close
74
+ end
75
+ end
76
+
77
+ # Atomic write
78
+ FileUtils.mv("#{filename}+", filename)
79
+
80
+ # Set mtime correctly
81
+ File.utime(@mtime, @mtime, filename)
82
+
83
+ nil
84
+ ensure
85
+ # Ensure tmp file gets cleaned up
86
+ FileUtils.rm("#{filename}+") if File.exist?("#{filename}+")
87
+ end
88
+
89
+ end
90
+ end
@@ -0,0 +1,74 @@
1
+ require 'fileutils'
2
+ require 'find'
3
+
4
+ module Spar
5
+ module Compiler
6
+
7
+ def self.assets
8
+ assets = []
9
+ Spar.sprockets.each_logical_path do |logical_path|
10
+ next unless compile_info = path_compile_info(logical_path)
11
+ if asset = Spar.sprockets.find_asset(logical_path)
12
+ assets << Spar::CompiledAsset.new(asset, compile_info)
13
+ end
14
+ end
15
+ Dir.chdir(Spar.root) do
16
+ if Dir.exists?('static')
17
+ Find.find('static').each do |path|
18
+ if FileTest.directory?(path)
19
+ if File.basename(path)[0] == '..'
20
+ Find.prune # Don't look any further into this directory.
21
+ else
22
+ next
23
+ end
24
+ else
25
+ if File.basename(path) == '.DS_Store'
26
+ next
27
+ else
28
+ assets << Spar::CompiledAsset.new(path)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ assets
35
+ end
36
+
37
+ def self.path_compile_info(logical_path)
38
+ if logical_path =~ /\.html/
39
+ return {
40
+ :digest => false,
41
+ :cache_control => 'no-cache'
42
+ }
43
+ elsif logical_path =~ /\w+\.(?!js|css).+/
44
+ return {
45
+ :digest => Spar.settings['digests'],
46
+ :cache_control => Spar.settings['cache_control']
47
+ }
48
+ elsif file_path = Spar.sprockets.resolve(logical_path)
49
+ file = File.open(file_path, "rb").read
50
+ if header = file[Sprockets::DirectiveProcessor::HEADER_PATTERN, 0]
51
+ if directive = header.lines.peek[Sprockets::DirectiveProcessor::DIRECTIVE_PATTERN, 1]
52
+ name, *args = Shellwords.shellwords(directive)
53
+ if name == 'deploy'
54
+ return deploy_directive_info(*args)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ nil
60
+ end
61
+
62
+ def self.deploy_directive_info(*args)
63
+ options = {}
64
+ args.each do |arg|
65
+ options[arg.split(':')[0]] = arg.split(':')[1]
66
+ end
67
+ {
68
+ :digest => Spar.settings['digests'],
69
+ :cache_control => options['cache_control'] || Spar.settings['cache_control']
70
+ }
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,18 @@
1
+ module Spar
2
+ module Compressor
3
+ class JS
4
+ def compress(source, options = {})
5
+ require 'uglifier'
6
+ Uglifier.compile(source, options.merge(Spar.settings['js_compressor']))
7
+ end
8
+ end
9
+
10
+ class CSS
11
+ def compress(source, options = {})
12
+ require 'yui/compressor'
13
+ compressor = YUI::CssCompressor.new(options.merge(Spar.settings['css_compressor']))
14
+ compressor.compress(source)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,33 @@
1
+ require 'cloudfront-invalidator'
2
+
3
+ class CloudfrontDeployer < S3Deployer
4
+
5
+ def prepare(assets)
6
+ super
7
+ @cloudfront_distribution = Spar.settings['cloudfront_distribution']
8
+ raise "ERROR: You should set a :cloudfront_distribution in your config.yml file so you can deploy to Cloudfront" unless @cloudfront_distribution
9
+ @invalidator = CloudfrontInvalidator.new(
10
+ @aws_key,
11
+ @aws_secret,
12
+ @cloudfront_distribution
13
+ )
14
+ end
15
+
16
+ def finish
17
+ to_invalidate = @deployed_assets.collect do |asset|
18
+ if asset.write_path =~ /\.html$/
19
+ [ asset.write_path, asset.write_path.gsub('index.html',''), asset.write_path.gsub('/index.html','') ]
20
+ else
21
+ asset.write_path
22
+ end
23
+ end
24
+ to_invalidate << 'TIMESTAMP.txt'
25
+
26
+ @invalidator.invalidate(to_invalidate.flatten) do |status,time|
27
+ puts "Invalidation #{status} after %.2f seconds" % time.to_f
28
+ end
29
+
30
+ super
31
+ end
32
+
33
+ end
@@ -0,0 +1,26 @@
1
+ module Spar
2
+ class Deployer
3
+
4
+ def run(assets)
5
+ prepare(assets)
6
+ @assets_to_deploy.each { |asset| deploy(asset) }
7
+ finish
8
+ end
9
+
10
+ def prepare(assets)
11
+ @assets_to_deploy = assets
12
+ @deployed_assets = []
13
+ end
14
+
15
+ def deploy(asset)
16
+ @deployed_assets << asset
17
+ end
18
+
19
+ def finish
20
+ @deployed_assets.each do |asset|
21
+ puts "Deployed Asset: #{asset.write_path}"
22
+ end
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ class LocalDeployer < Spar::Deployer
2
+
3
+ def prepare(assets)
4
+ super
5
+
6
+ @deploy_path = Spar.settings['deploy_path']
7
+ raise "ERROR: You should set the :deploy_path in your config.yml to a directory you want to do the local deploy to." unless @deploy_path
8
+ end
9
+
10
+ def deploy(asset)
11
+ asset.write_to(@deploy_path)
12
+ super
13
+ end
14
+
15
+ end
@@ -0,0 +1,75 @@
1
+ require 'aws-sdk'
2
+ require 'logger'
3
+
4
+ class S3Deployer < Spar::Deployer
5
+
6
+ def prepare(assets)
7
+ @aws_key = Spar.settings['aws_key']
8
+ @aws_secret = Spar.settings['aws_secret']
9
+ @deploy_bucket = Spar.settings['deploy_bucket']
10
+ unless @aws_key and @aws_secret and @deploy_bucket
11
+ raise "ERROR: You should set :aws_key, :aws_secret, and :deploy_bucket in your config.yml file so you can deploy to S3"
12
+ end
13
+
14
+ AWS.config(
15
+ :access_key_id => @aws_key,
16
+ :secret_access_key => @aws_secret,
17
+ :logger => Logger.new($stderr),
18
+ :log_formatter => AWS::Core::LogFormatter.colored
19
+ )
20
+ AWS.config.logger.level = Logger::WARN
21
+ @s3 = AWS::S3.new
22
+ @bucket = @s3.buckets[@deploy_bucket]
23
+ unless @bucket.exists?
24
+ @bucket = @s3.buckets.create(@deploy_bucket)
25
+ end
26
+ @age_out = 60 * 60 * 24 * 3 # 3 days
27
+
28
+ super
29
+
30
+ local_assets = assets.map(&:write_path)
31
+ remote_assets = @bucket.objects.map{ |object| object.key }.reject{ |key| key =~ /\/$/ }
32
+
33
+ age_out (remote_assets - local_assets)
34
+ end
35
+
36
+ def age_out(old_assets)
37
+ old_assets.flatten.each do |file|
38
+ if Time.now - @bucket.objects[file].last_modified > @age_out
39
+ logger "Deleting #{file}"
40
+ @bucket.objects[file].delete
41
+ end
42
+ end
43
+ end
44
+
45
+ def deploy(asset)
46
+ remote_file = @bucket.objects[asset.write_path]
47
+ if remote_file.exists?
48
+ if remote_file.etag.gsub(/\"/,'') != asset.digest
49
+ upload(asset)
50
+ super
51
+ end
52
+ else
53
+ upload(asset)
54
+ super
55
+ end
56
+ end
57
+
58
+ def upload(asset)
59
+ if asset.write_path =~ /\.html$/ && !(asset.write_path =~ /\/?index\.html$/)
60
+ @bucket.objects[asset.write_path.gsub(/\.html/, '/index.html')].write(asset.headers.merge(:data => asset.data))
61
+ else
62
+ @bucket.objects[asset.write_path].write(asset.headers.merge(:data => asset.data))
63
+ end
64
+ end
65
+
66
+ def finish
67
+ @bucket.objects['TIMESTAMP.txt'].write(
68
+ :data => Time.now.to_s+"\n",
69
+ :content_type => "text/plain",
70
+ :acl => :public_read
71
+ )
72
+ super
73
+ end
74
+
75
+ end