spar 0.2.7 → 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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