spar 0.2.7 → 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +272 -1
- data/bin/spar +30 -1
- data/lib/spar.rb +128 -7
- data/lib/spar/assets/404.jpg +0 -0
- data/lib/spar/assets/500.jpg +0 -0
- data/lib/spar/cli.rb +75 -22
- data/lib/spar/compiled_asset.rb +90 -0
- data/lib/spar/compiler.rb +74 -0
- data/lib/spar/compressor.rb +18 -0
- data/lib/spar/deployers/cloudfront_deployer.rb +33 -0
- data/lib/spar/deployers/deployer.rb +26 -0
- data/lib/spar/deployers/local_deployer.rb +15 -0
- data/lib/spar/deployers/s3_deployer.rb +75 -0
- data/lib/spar/directive_processor.rb +36 -0
- data/lib/spar/exceptions.rb +330 -0
- data/lib/spar/generators/templates/{README → base/README} +0 -0
- data/lib/spar/generators/templates/base/config.yml +12 -0
- data/lib/spar/generators/templates/pages/haml/app/pages/index.html.haml +11 -0
- data/lib/spar/generators/templates/pages/html/app/pages/index.html +16 -0
- data/lib/spar/generators/templates/{app/assets/javascripts/javascript.js.coffee → scripts/coffee/app/javascripts/application.js.coffee} +6 -3
- data/lib/spar/generators/templates/scripts/js/app/javascripts/application.js +11 -0
- data/lib/spar/generators/templates/styles/css/app/stylesheets/application.css +18 -0
- data/lib/spar/generators/templates/{app/assets → styles/sass/app}/stylesheets/application.css.sass +8 -4
- data/lib/spar/helpers.rb +31 -102
- data/lib/spar/not_found.rb +64 -0
- data/lib/spar/rewrite.rb +29 -0
- data/lib/spar/static.rb +46 -0
- data/lib/spar/tasks.rb +8 -13
- data/lib/spar/version.rb +1 -1
- metadata +261 -66
- data/lib/spar/assets.rb +0 -78
- data/lib/spar/base.rb +0 -54
- data/lib/spar/css_compressor.rb +0 -17
- data/lib/spar/deployer.rb +0 -198
- data/lib/spar/generators/application.rb +0 -42
- data/lib/spar/generators/templates/Gemfile +0 -3
- data/lib/spar/generators/templates/Rakefile +0 -8
- data/lib/spar/generators/templates/app/views/index.haml +0 -7
- data/lib/spar/generators/templates/config.ru +0 -9
- data/lib/spar/generators/templates/config/application.rb +0 -12
- data/lib/spar/generators/templates/script/server +0 -1
- data/lib/spar/static_compiler.rb +0 -91
Binary file
|
Binary file
|
data/lib/spar/cli.rb
CHANGED
@@ -1,32 +1,85 @@
|
|
1
|
-
require '
|
1
|
+
require 'pathname'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'listen'
|
4
|
+
require 'rack'
|
5
|
+
require 'thor'
|
2
6
|
|
3
|
-
|
4
|
-
|
5
|
-
|
7
|
+
module Spar
|
8
|
+
class CLI < Thor
|
9
|
+
include Thor::Actions
|
6
10
|
|
7
|
-
|
11
|
+
def self.source_root
|
12
|
+
File.expand_path('../../..', __FILE__)
|
13
|
+
end
|
8
14
|
|
9
|
-
|
10
|
-
#{desc}
|
15
|
+
desc 'server', 'Serve spar application'
|
11
16
|
|
12
|
-
|
17
|
+
method_option :port, :aliases => '-p', :desc => 'Port'
|
13
18
|
|
14
|
-
|
15
|
-
|
16
|
-
|
19
|
+
def server
|
20
|
+
Rack::Server.start(
|
21
|
+
:Port => options[:port] || 8888,
|
22
|
+
:app => Spar.app
|
23
|
+
)
|
24
|
+
end
|
17
25
|
|
18
|
-
|
26
|
+
desc 'deploy', 'Deploy the project.'
|
19
27
|
|
20
|
-
|
21
|
-
|
22
|
-
exit(0)
|
23
|
-
end
|
28
|
+
def deploy(environment='production')
|
29
|
+
say "Deploying: #{Spar.root}"
|
24
30
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
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
|
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
|