sphere 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.markdown +84 -0
- data/Rakefile +26 -0
- data/VERSION +1 -0
- data/lib/sphere.rb +32 -0
- data/lib/sphere/backends/base.rb +44 -0
- data/lib/sphere/backends/closure.rb +29 -0
- data/lib/sphere/backends/yui.rb +28 -0
- data/lib/sphere/config.rb +55 -0
- data/lib/sphere/css_minifier.rb +29 -0
- data/lib/sphere/helper.rb +38 -0
- data/lib/sphere/package/base.rb +87 -0
- data/lib/sphere/package/javascript.rb +17 -0
- data/lib/sphere/package/stylesheet.rb +17 -0
- data/lib/sphere/packager.rb +48 -0
- data/lib/sphere/tasks/commit.rake +22 -0
- data/lib/sphere/tasks/compass.rake +19 -0
- data/lib/sphere/tasks/sphere.rake +31 -0
- data/lib/sphere/tasks/upload.rake +37 -0
- data/rails/init.rb +2 -0
- data/spec/scenario/config/boot.rb +110 -0
- data/spec/scenario/config/database.yml +0 -0
- data/spec/scenario/config/environment.rb +5 -0
- data/spec/scenario/config/sphere.yml +16 -0
- data/spec/scenario/public/javascripts/custom/file_a.js +9 -0
- data/spec/scenario/public/javascripts/custom/file_b.js +8 -0
- data/spec/scenario/public/javascripts/custom/ignore.js +8 -0
- data/spec/scenario/public/javascripts/file.js +8 -0
- data/spec/scenario/public/stylesheets/compiled/custom.css +1 -0
- data/spec/scenario/public/stylesheets/compiled/print.css +40 -0
- data/spec/spec_helper.rb +50 -0
- data/spec/sphere/config_spec.rb +27 -0
- data/spec/sphere/css_minifier_spec.rb +16 -0
- data/spec/sphere/helper_spec.rb +57 -0
- data/spec/sphere/package/base_spec.rb +46 -0
- data/spec/sphere/package/javascript_spec.rb +19 -0
- data/spec/sphere/package/stylesheet_spec.rb +19 -0
- data/spec/sphere/packager_spec.rb +44 -0
- data/spec/sphere_spec.rb +16 -0
- data/sphere.gemspec +90 -0
- metadata +112 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
pkg
|
data/README.markdown
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Sphere
|
2
|
+
|
3
|
+
Sphere is a asset packer and compressor, for Rails, built (primarily) to be used with Heroku + AWS/S3.
|
4
|
+
Inspired by [Jammit](http://documentcloud.github.com/jammit) and [Compass](http://wiki.github.com/chriseppstein/compass)
|
5
|
+
|
6
|
+
## Quick Start
|
7
|
+
|
8
|
+
### Create a `config/shere.yml` file and define your packages. Here an example file:
|
9
|
+
|
10
|
+
# Use packaged assets in views [production: true, other: false].
|
11
|
+
# Hint: Don't hard-code option this in the configuration file,
|
12
|
+
# instead use your config/environments/* files.
|
13
|
+
# Example:
|
14
|
+
# # config/environments/staging.rb
|
15
|
+
# # ...
|
16
|
+
# Sphere.config.package = true
|
17
|
+
# package: true
|
18
|
+
|
19
|
+
# Where (within Rails.public_path) to store packaged assets [assets -> public/assets]
|
20
|
+
asset_path: assets
|
21
|
+
|
22
|
+
# Compression backend [closure]
|
23
|
+
backend: closure
|
24
|
+
|
25
|
+
# Use compression when packaging assets [production: true, other: false]
|
26
|
+
compress: true
|
27
|
+
|
28
|
+
# Define your javascript packages here:
|
29
|
+
javascripts:
|
30
|
+
# Example assuming default settings (Rails.public_path: public, asset_path: assets):
|
31
|
+
# Fetch content from http://ajax.googleapis.com and merge/compress it with all files
|
32
|
+
# matching public/javascripts/vendor/*.js + public/javascripts/custom/*.js into public/assets/application.js
|
33
|
+
application:
|
34
|
+
- http://ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.js
|
35
|
+
- javascripts/vendor/*.js
|
36
|
+
- javascripts/custom/*.js
|
37
|
+
|
38
|
+
# Define your stylesheet packages here:
|
39
|
+
stylesheets:
|
40
|
+
# Example assuming default settings (Rails.public_path: public, asset_path: assets):
|
41
|
+
# Merge/compress public/stylesheets/compiled/screen.css into public/assets/screen.css
|
42
|
+
# Merge/compress public/stylesheets/compiled/print.css into public/assets/print.css
|
43
|
+
screen:
|
44
|
+
- stylesheets/compiled/screen.css
|
45
|
+
print:
|
46
|
+
- stylesheets/compiled/print.css
|
47
|
+
|
48
|
+
### In your layout/application.html.erb
|
49
|
+
|
50
|
+
<%= include_stylesheets :screen, :media => 'screen, projection' %>
|
51
|
+
<%= include_stylesheets :print, :media => 'print' %>
|
52
|
+
<%= include_javascripts :application %>
|
53
|
+
|
54
|
+
If the package_assets option is enabled (production), this will produce (given that config.action_controller.asset_host = "http://my-asset-host-bucket.s3.amazonaws.com"):
|
55
|
+
|
56
|
+
<link href="http://my-asset-host-bucket.s3.amazonaws.com/assets/screen.css" media="screen, projection" rel="stylesheet" type="text/css" />
|
57
|
+
<link href="http://my-asset-host-bucket.s3.amazonaws.com/assets/print.css" media="print" rel="stylesheet" type="text/css" />
|
58
|
+
<script src="http://my-asset-host-bucket.s3.amazonaws.com/assets/application.js" type="text/javascript"></script>
|
59
|
+
|
60
|
+
If the package_assets option is disabled (development), this will produce:
|
61
|
+
|
62
|
+
<link href="/stylesheets/compiled/screen.css" media="screen, projection" rel="stylesheet" type="text/css" />
|
63
|
+
<link href="/stylesheets/compiled/print.css" media="print" rel="stylesheet" type="text/css" />
|
64
|
+
|
65
|
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.js" type="text/javascript"></script>
|
66
|
+
<script src="/javascripts/vendor/jquery.enumerable.js" type="text/javascript"></script>
|
67
|
+
<script src="/javascripts/custom/rails.js" type="text/javascript"></script>
|
68
|
+
|
69
|
+
## Include Rake Tasks
|
70
|
+
|
71
|
+
Create a new `lib/tasks/sphere.rake` file in your Rails application. Include the following lines:
|
72
|
+
|
73
|
+
# Adds the sphere:compile task
|
74
|
+
load 'sphere/tasks/sphere.rake'
|
75
|
+
|
76
|
+
# [Optional] Extends the sphere:compile:css task; pre-compiles Compass/SASS files to CSS before packaging
|
77
|
+
load 'sphere/tasks/compass.rake'
|
78
|
+
|
79
|
+
# [Optional] Adds sphere:upload & sphere:deploy tasks for automated S3 upload
|
80
|
+
load 'sphere/tasks/upload.rake'
|
81
|
+
|
82
|
+
# [Optional] Adds sphere:commit:hook task you can run to ensure all assets are up-to-date.
|
83
|
+
# Just e.g. add "rake sphere:commit:hook -s" to your .git/hooks/pre-commit hook.
|
84
|
+
load 'sphere/tasks/commit.rake'
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec/rake/spectask'
|
3
|
+
require 'spec/version'
|
4
|
+
|
5
|
+
desc 'Default: run specs.'
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
Spec::Rake::SpecTask.new do |t|
|
9
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
10
|
+
t.spec_opts = ['--colour', '--format', 'profile', '--timeout', '20']
|
11
|
+
end
|
12
|
+
|
13
|
+
begin
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gemspec|
|
16
|
+
gemspec.name = "sphere"
|
17
|
+
gemspec.summary = "Asset packer and compressor, for Rails"
|
18
|
+
gemspec.description = "Built (primarily) to be used with Heroku + AWS/S3"
|
19
|
+
gemspec.email = "dimitrij@blacksquaremedia.com"
|
20
|
+
gemspec.homepage = "http://github.com/dim/sphere"
|
21
|
+
gemspec.authors = ["Dimitrij Denissenko"]
|
22
|
+
end
|
23
|
+
Jeweler::GemcutterTasks.new
|
24
|
+
rescue LoadError
|
25
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
26
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/sphere.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
|
3
|
+
module Sphere
|
4
|
+
autoload :Config, 'sphere/config'
|
5
|
+
autoload :Packager, 'sphere/packager'
|
6
|
+
autoload :Helper, 'sphere/helper'
|
7
|
+
autoload :CSSMinifier, 'sphere/css_minifier'
|
8
|
+
|
9
|
+
module Package
|
10
|
+
autoload :Base, 'sphere/package/base'
|
11
|
+
autoload :Javascript, 'sphere/package/javascript'
|
12
|
+
autoload :Stylesheet, 'sphere/package/stylesheet'
|
13
|
+
end
|
14
|
+
|
15
|
+
module Backends
|
16
|
+
autoload :Base, 'sphere/backends/base'
|
17
|
+
autoload :Closure, 'sphere/backends/closure'
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.config
|
21
|
+
@config ||= Config.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.reset!
|
25
|
+
@config = nil
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.packager
|
30
|
+
Sphere::Packager.instance
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module Sphere
|
4
|
+
module Backends
|
5
|
+
class Base
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def default_options
|
10
|
+
{}
|
11
|
+
end
|
12
|
+
|
13
|
+
def uri
|
14
|
+
raise "Abstract Method"
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :content
|
20
|
+
|
21
|
+
def initialize(content)
|
22
|
+
@content = content
|
23
|
+
end
|
24
|
+
|
25
|
+
def compressed(options = {})
|
26
|
+
params = create_params!(self.class.default_options.merge(options))
|
27
|
+
response = Net::HTTP.post_form self.class.uri, params
|
28
|
+
unless response.code == '200'
|
29
|
+
STDERR.puts "ERROR during compilation. Response:"
|
30
|
+
STDERR.puts response.body
|
31
|
+
raise "Aborting!"
|
32
|
+
end
|
33
|
+
response.body
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def create_params!(options)
|
39
|
+
options.to_a
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module Sphere
|
4
|
+
module Backends
|
5
|
+
class Closure < Base
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def default_options
|
10
|
+
{ :compilation_level => 'SIMPLE_OPTIMIZATIONS',
|
11
|
+
:output_format => 'text',
|
12
|
+
:output_info => 'compiled_code' }
|
13
|
+
end
|
14
|
+
|
15
|
+
def uri
|
16
|
+
@uri ||= URI.parse('http://closure-compiler.appspot.com/compile')
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def create_params!(options)
|
24
|
+
options.merge(:js_code => content).to_a
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module Sphere
|
4
|
+
module Backends
|
5
|
+
class YUI < Base
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def default_options
|
10
|
+
{ :type => 'JS' }
|
11
|
+
end
|
12
|
+
|
13
|
+
def uri
|
14
|
+
@uri ||= URI.parse('http://refresh-sf.com/yui/')
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def create_params!(options)
|
22
|
+
options.merge(:compresstext => content).to_a
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Sphere
|
2
|
+
class Config
|
3
|
+
attr_accessor :env, :root
|
4
|
+
attr_writer :config_file, :asset_path, :backend, :compress, :package
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
self.env = defined?(Rails) ? Rails.env.to_s : 'development'
|
8
|
+
self.root = defined?(Rails) ? Rails.root : Pathname.new('.')
|
9
|
+
end
|
10
|
+
|
11
|
+
def update!(&block)
|
12
|
+
block.call(self)
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def config_file
|
17
|
+
@config_file || root.join('config', 'sphere.yml')
|
18
|
+
end
|
19
|
+
|
20
|
+
def asset_path
|
21
|
+
@asset_path || public_path.join(definitions['asset_path'] || 'assets')
|
22
|
+
end
|
23
|
+
|
24
|
+
def backend
|
25
|
+
@backend || "Sphere::Backends::#{(definitions['backend'] || 'closure').classify}".constantize
|
26
|
+
end
|
27
|
+
|
28
|
+
def javascripts
|
29
|
+
definitions['javascripts'] || {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def stylesheets
|
33
|
+
definitions['stylesheets'] || {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def compress?
|
37
|
+
defined?(@compress) ? !!@compress : (definitions.key?('compress') ? !!definitions['compress'] : (self.env == 'production'))
|
38
|
+
end
|
39
|
+
|
40
|
+
def package?
|
41
|
+
defined?(@package) ? !!@package : (definitions.key?('package') ? !!definitions['package'] : (self.env == 'production'))
|
42
|
+
end
|
43
|
+
|
44
|
+
def public_path
|
45
|
+
@public_path ||= defined?(Rails) ? Pathname.new(Rails.public_path.to_s) : root.join('public')
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def definitions
|
51
|
+
@definitions ||= YAML.load_file(config_file)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Sphere
|
2
|
+
class CSSMinifier < String
|
3
|
+
|
4
|
+
|
5
|
+
def initialize(css)
|
6
|
+
super(css.dup)
|
7
|
+
end
|
8
|
+
|
9
|
+
def minify!
|
10
|
+
gsub!(/\/\*.+?\*\//m, '')
|
11
|
+
strip!
|
12
|
+
gsub!(/(\A|\})(.+?)(\z|\{)/m) do |m|
|
13
|
+
prefix, tags, suffix = $1, $2, $3
|
14
|
+
"#{prefix}#{tags.split(',').map(&:strip).reject(&:blank?).join(',')}#{suffix}"
|
15
|
+
end
|
16
|
+
gsub!(/\{(.+?)\}/m) do |m|
|
17
|
+
styles = $1.split(';').map do |style|
|
18
|
+
key, values = style.split(':')
|
19
|
+
next nil unless key && values
|
20
|
+
"#{key.strip}:#{values.split(',').map(&:strip).reject(&:blank?).join(',')}"
|
21
|
+
end.compact.join(';')
|
22
|
+
"{#{styles};}"
|
23
|
+
end
|
24
|
+
gsub!(/[\r\n]/m, '')
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Sphere
|
2
|
+
module Helper
|
3
|
+
|
4
|
+
def include_javascripts(*package_names)
|
5
|
+
options = package_names.extract_options!
|
6
|
+
tags = package_names.map do |name|
|
7
|
+
if Sphere.config.package?
|
8
|
+
compute_public_path(name.to_s, Sphere.config.asset_path, Sphere::Package::Javascript.extension)
|
9
|
+
else
|
10
|
+
individual_includable_urls(name, :js, options)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
javascript_include_tag(*(tags.flatten << options))
|
14
|
+
end
|
15
|
+
|
16
|
+
def include_stylesheets(*package_names)
|
17
|
+
options = package_names.extract_options!
|
18
|
+
tags = package_names.map do |name|
|
19
|
+
if Sphere.config.package?
|
20
|
+
compute_public_path(name.to_s, Sphere.config.asset_path, Sphere::Package::Stylesheet.extension)
|
21
|
+
else
|
22
|
+
individual_includable_urls(name, :css, options)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
stylesheet_link_tag(*(tags.flatten << options))
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def individual_includable_urls(package_name, extension, options = {})
|
31
|
+
package = Sphere.packager.packages(extension.to_sym)[package_name.to_s]
|
32
|
+
(package ? package.sources : []).map do |source|
|
33
|
+
compute_public_path(source.to_s, '', extension.to_s).sub(/^\/\//, '/')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Sphere
|
2
|
+
module Package
|
3
|
+
class Base
|
4
|
+
|
5
|
+
def self.extension
|
6
|
+
raise "Abstract Method"
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
def initialize(name, sources)
|
12
|
+
@name = name
|
13
|
+
@sources = sources
|
14
|
+
end
|
15
|
+
|
16
|
+
def file
|
17
|
+
@file ||= Sphere.config.asset_path.join("#{name}.#{self.class.extension}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def public_file_name
|
21
|
+
file.to_s.sub "#{Sphere.config.public_path}/", ''
|
22
|
+
end
|
23
|
+
|
24
|
+
def out_of_date?
|
25
|
+
!file.exist? || mtime > file.mtime
|
26
|
+
end
|
27
|
+
|
28
|
+
def compile(options = {})
|
29
|
+
unless out_of_date?
|
30
|
+
STDOUT.puts "unchanged #{public_file_name}" if options[:verbose] == true
|
31
|
+
return unless options[:force] == true
|
32
|
+
end
|
33
|
+
|
34
|
+
STDOUT.puts "\e[32m compile #{public_file_name}\e[0m" if options[:verbose] == true
|
35
|
+
content = ""
|
36
|
+
expanded_sources.each do |source|
|
37
|
+
content << open(source).read
|
38
|
+
end
|
39
|
+
content = compress(content) if Sphere.config.compress?
|
40
|
+
|
41
|
+
FileUtils.mkdir_p(File.dirname(file))
|
42
|
+
file.open('w') {|f| f.write(content) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def sources
|
46
|
+
@sources.map do |source|
|
47
|
+
url?(source) ? source : relative_paths(source)
|
48
|
+
end.flatten
|
49
|
+
end
|
50
|
+
|
51
|
+
def source_files
|
52
|
+
expanded_sources - source_urls
|
53
|
+
end
|
54
|
+
|
55
|
+
def source_urls
|
56
|
+
expanded_sources.select {|s| url?(s) }
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def expanded_sources
|
62
|
+
sources.map do |source|
|
63
|
+
url?(source) ? source : Sphere.config.public_path.join(source)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def relative_paths(glob)
|
68
|
+
Dir[Sphere.config.public_path.join(glob)].sort.map do |path|
|
69
|
+
path.sub "#{Sphere.config.public_path}/", ''
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def mtime
|
74
|
+
(source_files.map(&:mtime) + [Sphere.config.config_file.mtime]).max
|
75
|
+
end
|
76
|
+
|
77
|
+
def compress(content)
|
78
|
+
content
|
79
|
+
end
|
80
|
+
|
81
|
+
def url?(source)
|
82
|
+
source =~ /^https?\:/
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|