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.
- 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
|