patch-asset_library 0.5.0.1
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.rdoc +220 -0
- data/lib/asset_library.rb +121 -0
- data/lib/asset_library/asset.rb +35 -0
- data/lib/asset_library/asset_module.rb +94 -0
- data/lib/asset_library/compiler.rb +37 -0
- data/lib/asset_library/compiler/base.rb +20 -0
- data/lib/asset_library/compiler/closure.rb +41 -0
- data/lib/asset_library/compiler/default.rb +15 -0
- data/lib/asset_library/helpers.rb +98 -0
- data/lib/asset_library/rake_tasks.rb +22 -0
- data/lib/asset_library/util.rb +26 -0
- data/rails/init.rb +4 -0
- data/spec/asset_library/asset_module_spec.rb +183 -0
- data/spec/asset_library/asset_spec.rb +49 -0
- data/spec/asset_library/compiler/base_spec.rb +11 -0
- data/spec/asset_library/compiler/closure_spec.rb +104 -0
- data/spec/asset_library/compiler/default_spec.rb +27 -0
- data/spec/asset_library/compiler_spec.rb +35 -0
- data/spec/asset_library/helpers_spec.rb +210 -0
- data/spec/asset_library_spec.rb +120 -0
- data/spec/integration_spec.rb +90 -0
- data/spec/spec_helper.rb +70 -0
- metadata +98 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/compiler/base'
|
2
|
+
require File.dirname(__FILE__) + '/compiler/default'
|
3
|
+
require File.dirname(__FILE__) + '/compiler/closure'
|
4
|
+
|
5
|
+
class AssetLibrary
|
6
|
+
module Compiler
|
7
|
+
Error = Class.new(RuntimeError)
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# Create an instance of a compiler for the given compiler type.
|
11
|
+
def create(type, config={})
|
12
|
+
klass = compiler_classes[type] ||= built_in_class_for(type)
|
13
|
+
klass.new(config)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Register a custom compiler class.
|
17
|
+
def register(type, klass)
|
18
|
+
compiler_classes[type] = klass
|
19
|
+
end
|
20
|
+
|
21
|
+
def reset!
|
22
|
+
compiler_classes.clear
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def compiler_classes
|
28
|
+
@compiler_classes ||= {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def built_in_class_for(type)
|
32
|
+
class_name = type.to_s.gsub(/(?:\A|_)(.)/){$1.upcase}
|
33
|
+
const_get(class_name)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class AssetLibrary
|
2
|
+
module Compiler
|
3
|
+
class Base
|
4
|
+
def initialize(config)
|
5
|
+
@config = config || {}
|
6
|
+
@asset_modules = []
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :config, :asset_modules
|
10
|
+
|
11
|
+
def add_asset_module(asset_module)
|
12
|
+
@asset_modules << asset_module
|
13
|
+
end
|
14
|
+
|
15
|
+
def write_all_caches(format = nil)
|
16
|
+
raise NotImplementedError, "abstract method"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
|
3
|
+
class AssetLibrary
|
4
|
+
module Compiler
|
5
|
+
class Closure < Base
|
6
|
+
def initialize(config)
|
7
|
+
super
|
8
|
+
config[:path] or
|
9
|
+
raise ConfigurationError, "Please set path of closure jar with compilers.closure.path configuration setting"
|
10
|
+
config[:java] ||= 'java'
|
11
|
+
config[:java_flags] = Util.normalize_flags(config[:java_flags])
|
12
|
+
config[:path] = normalize_path(config[:path])
|
13
|
+
config[:flags] = Util.normalize_flags(config[:flags])
|
14
|
+
end
|
15
|
+
|
16
|
+
def write_all_caches(format = nil)
|
17
|
+
asset_modules.each do |asset_module|
|
18
|
+
asset_module.each_compilation do |inputs, output|
|
19
|
+
command = [config[:java]]
|
20
|
+
command.concat(config[:java_flags])
|
21
|
+
command << '-jar' << config[:path]
|
22
|
+
command.concat(config[:flags])
|
23
|
+
command.concat(asset_module.compiler_flags)
|
24
|
+
command << '--js_output_file' << "#{output}"
|
25
|
+
inputs.each do |input|
|
26
|
+
command << '--js' << input
|
27
|
+
end
|
28
|
+
system *command or
|
29
|
+
raise Error, "closure compiler failed"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def normalize_path(path)
|
37
|
+
(Pathname(AssetLibrary.app_root) + path).to_s
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class AssetLibrary
|
2
|
+
module Compiler
|
3
|
+
class Default < Base
|
4
|
+
def write_all_caches(format = nil)
|
5
|
+
asset_modules.each do |asset_module|
|
6
|
+
asset_module.each_compilation do |inputs, output|
|
7
|
+
open(output, 'w') do |file|
|
8
|
+
inputs.each { |path| file << File.read(path) }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
|
3
|
+
class AssetLibrary
|
4
|
+
module Helpers
|
5
|
+
def asset_library_javascript_tags(module_key, format = nil)
|
6
|
+
|
7
|
+
m = AssetLibrary.asset_module(module_key)
|
8
|
+
if AssetLibrary.cache
|
9
|
+
AssetLibrary.cache_vars[:javascript_tags] ||= {}
|
10
|
+
AssetLibrary.cache_vars[:javascript_tags][module_key] ||= asset_library_priv.script_tag(m.cache_asset)
|
11
|
+
else
|
12
|
+
m.assets(format).collect{|a| asset_library_priv.script_tag(a)}.join("\n")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def asset_library_stylesheet_tags(module_key, *args)
|
17
|
+
html_options = args.last.is_a?(Hash) ? args.pop : {}
|
18
|
+
format = args[0]
|
19
|
+
|
20
|
+
m = AssetLibrary.asset_module(module_key)
|
21
|
+
if AssetLibrary.cache
|
22
|
+
AssetLibrary.cache_vars[:stylesheet_tags] ||= {}
|
23
|
+
AssetLibrary.cache_vars[:stylesheet_tags][[module_key, format, request.protocol, request.host_with_port]] ||= asset_library_priv.style_tag(m.cache_asset(format), html_options)
|
24
|
+
else
|
25
|
+
asset_library_priv.import_styles_tag(m.assets(format), html_options)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def asset_library_priv
|
32
|
+
@asset_library_priv ||= Priv.new(self)
|
33
|
+
end
|
34
|
+
|
35
|
+
class Priv
|
36
|
+
# Don't pollute helper's class's namespace with all our methods; put
|
37
|
+
# them here instead
|
38
|
+
|
39
|
+
attr_accessor :helper
|
40
|
+
|
41
|
+
def initialize(helper)
|
42
|
+
@helper = helper
|
43
|
+
end
|
44
|
+
|
45
|
+
def url(asset)
|
46
|
+
absolute_url(asset.relative_url)
|
47
|
+
end
|
48
|
+
|
49
|
+
def absolute_url(relative_url)
|
50
|
+
host = helper.__send__(:compute_asset_host, relative_url) if helper.respond_to?(:compute_asset_host, true)
|
51
|
+
|
52
|
+
host = nil if host == '' # Rails sets '' by default
|
53
|
+
|
54
|
+
if host && !(host =~ %r{^[-a-z]+://})
|
55
|
+
controller = helper.instance_variable_get(:@controller)
|
56
|
+
request = controller && controller.respond_to?(:request) && controller.request
|
57
|
+
host = request && "#{request.protocol}#{host}"
|
58
|
+
end
|
59
|
+
|
60
|
+
if host
|
61
|
+
"#{host}#{relative_url}"
|
62
|
+
else
|
63
|
+
relative_url
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def script_tag(asset)
|
68
|
+
content_tag(:script, "", {:type => "text/javascript", :src => url(asset)})
|
69
|
+
end
|
70
|
+
|
71
|
+
def style_tag(asset, html_options = {})
|
72
|
+
"<link rel=\"stylesheet\" type=\"text/css\" href=\"#{url(asset)}\" #{attributes_from_hash(html_options)}/>"
|
73
|
+
end
|
74
|
+
|
75
|
+
def import_styles_tag(assets, html_options = {})
|
76
|
+
a = []
|
77
|
+
assets.each_slice(30) do |subset|
|
78
|
+
a << import_style_tag(subset, html_options)
|
79
|
+
end
|
80
|
+
a.join("\n")
|
81
|
+
end
|
82
|
+
|
83
|
+
def import_style_tag(assets, html_options = {})
|
84
|
+
imports = assets.collect{ |a| "@import \"#{url(a)}\";" }
|
85
|
+
content_tag(:style, "\n#{imports.join("\n")}\n", html_options.merge(:type => "text/css"))
|
86
|
+
end
|
87
|
+
|
88
|
+
def content_tag(name, content, options = {})
|
89
|
+
"<#{name} #{attributes_from_hash(options)}>#{content}</#{name}>"
|
90
|
+
end
|
91
|
+
|
92
|
+
def attributes_from_hash(options = {})
|
93
|
+
options.to_a.collect{|k, v| "#{k}=\"#{v}\""}.join(" ")
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
def init_asset_library
|
2
|
+
require 'asset_library'
|
3
|
+
|
4
|
+
# TODO: Find a way to not-hard-code these paths?
|
5
|
+
AssetLibrary.config_path = Rails.root + 'config/asset_library.yml'
|
6
|
+
AssetLibrary.root = Rails.public_path
|
7
|
+
AssetLibrary.app_root = Rails.root
|
8
|
+
end
|
9
|
+
|
10
|
+
namespace(:asset_library) do
|
11
|
+
desc "Writes all asset caches specified in config/asset.yml by concatenating the constituent files."
|
12
|
+
task(:write) do
|
13
|
+
init_asset_library
|
14
|
+
AssetLibrary.write_all_caches
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Deletes all asset caches specified in config/asset.yml"
|
18
|
+
task(:clean) do
|
19
|
+
init_asset_library
|
20
|
+
AssetLibrary.delete_all_caches
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
class AssetLibrary
|
4
|
+
module Util
|
5
|
+
class << self
|
6
|
+
def symbolize_hash_keys(hash)
|
7
|
+
return hash unless Hash === hash # because we recurse
|
8
|
+
hash.inject({}) do |ret, (key, value)|
|
9
|
+
ret[(key.to_sym rescue key) || key] = symbolize_hash_keys(value)
|
10
|
+
ret
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def normalize_flags(flags)
|
15
|
+
case flags
|
16
|
+
when String
|
17
|
+
Shellwords.shellwords(flags)
|
18
|
+
when nil
|
19
|
+
[]
|
20
|
+
else
|
21
|
+
flags
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
describe(AssetLibrary::AssetModule) do
|
6
|
+
before(:each) do
|
7
|
+
AssetLibrary.stub!(:root).and_return(prefix)
|
8
|
+
end
|
9
|
+
|
10
|
+
after(:each) do
|
11
|
+
wipe_fs
|
12
|
+
end
|
13
|
+
|
14
|
+
describe('#assets') do
|
15
|
+
it('should include file1 and file2') do
|
16
|
+
files = [ '/c/file1.css', '/c/file2.css' ]
|
17
|
+
stub_fs(files)
|
18
|
+
m(css_config(:files => ['file1', 'file2'])).assets.collect{|a| a.absolute_path}.should == ["#{prefix}/c/file1.css", "#{prefix}/c/file2.css"]
|
19
|
+
end
|
20
|
+
|
21
|
+
it('should not include file2 if that does not exist') do
|
22
|
+
files = [ '/c/file1.css' ]
|
23
|
+
stub_fs(files)
|
24
|
+
m(css_config(:files => ['file1', 'file2'])).assets.collect{|a| a.absolute_path}.should == [ "#{prefix}/c/file1.css" ]
|
25
|
+
end
|
26
|
+
|
27
|
+
it('should not include other files') do
|
28
|
+
files = [ '/c/file1.css', '/c/file2.css' ]
|
29
|
+
stub_fs(files)
|
30
|
+
m(css_config(:files => ['file1'])).assets.collect{|a| a.absolute_path}.should == [ "#{prefix}/c/file1.css" ]
|
31
|
+
end
|
32
|
+
|
33
|
+
it('should glob filenames') do
|
34
|
+
files = [ '/c/file1.css', '/c/file2.css', '/c/other_file.css' ]
|
35
|
+
stub_fs(files)
|
36
|
+
m(css_config(:files => ['file*'])).assets.collect{|a| a.absolute_path}.should == ["#{prefix}/c/file1.css", "#{prefix}/c/file2.css"]
|
37
|
+
end
|
38
|
+
|
39
|
+
it('should glob directories') do
|
40
|
+
files = [ '/c/file1.css', '/c/a/file2.css', '/c/b/a/file3.css' ]
|
41
|
+
stub_fs(files)
|
42
|
+
m(css_config(:files => ['**/file*'])).assets.collect{|a| a.absolute_path}.should == ["#{prefix}/c/a/file2.css", "#{prefix}/c/b/a/file3.css", "#{prefix}/c/file1.css"]
|
43
|
+
end
|
44
|
+
|
45
|
+
it('should use :optional_suffix when appropriate') do
|
46
|
+
files = [ '/c/file1.css', '/c/file1.css.o' ]
|
47
|
+
stub_fs(files)
|
48
|
+
m(css_config(:optional_suffix => 'o', :files => ['file1'])).assets.collect{|a| a.absolute_path}.should == ["#{prefix}/c/file1.css.o"]
|
49
|
+
end
|
50
|
+
|
51
|
+
it('should show :optional_suffix file even if original is absent') do
|
52
|
+
files = [ '/c/file1.css.o' ]
|
53
|
+
stub_fs(files)
|
54
|
+
m(css_config(:optional_suffix => 'o', :files => ['file1'])).assets.collect{|a| a.absolute_path}.should == ["#{prefix}/c/file1.css.o"]
|
55
|
+
end
|
56
|
+
|
57
|
+
it('should ignore :optional_suffix when suffixed file is not present') do
|
58
|
+
stub_fs([ '/c/file1.css' ])
|
59
|
+
m(css_config(:optional_suffix => 'o', :files => ['file1'])).assets.collect{|a| a.absolute_path}.should == [ "#{prefix}/c/file1.css" ]
|
60
|
+
end
|
61
|
+
|
62
|
+
it('should pick files with :extra_suffix') do
|
63
|
+
stub_fs([ '/c/file1.e.css' ])
|
64
|
+
m(css_config(:files => ['file1'])).assets_with_extra_suffix('e').collect{|a| a.absolute_path}.should == [ "#{prefix}/c/file1.e.css" ]
|
65
|
+
end
|
66
|
+
|
67
|
+
it('should ignore non-suffixed files when :extra_suffix is set') do
|
68
|
+
stub_fs([ '/c/file1.css' ])
|
69
|
+
m(css_config(:files => ['file1'])).assets_with_extra_suffix('e').collect{|a| a.absolute_path}.should == []
|
70
|
+
end
|
71
|
+
|
72
|
+
it('should use extra suffixes with format') do
|
73
|
+
stub_fs([ '/c/file1.e1.css', '/c/file1.e2.css' ])
|
74
|
+
m(css_config(:files => ['file1'], :formats => { :f1 => [ 'e1', 'e2' ] })).assets_with_format(:f1).collect{|a| a.absolute_path}.should == [ "#{prefix}/c/file1.e1.css", "#{prefix}/c/file1.e2.css" ]
|
75
|
+
end
|
76
|
+
|
77
|
+
it('should ignore extra suffixes unspecified in format') do
|
78
|
+
stub_fs([ '/c/file1.e1.css', '/c/file1.e2.css' ])
|
79
|
+
m(css_config(:files => ['file1'], :formats => { :f1 => [ 'e1' ] })).assets_with_format(:f1).collect{|a| a.absolute_path}.should == [ "#{prefix}/c/file1.e1.css" ]
|
80
|
+
end
|
81
|
+
|
82
|
+
it('should allow nil suffixes in format') do
|
83
|
+
stub_fs([ '/c/file1.css', '/c/file1.e1.css' ])
|
84
|
+
m(css_config(:files => ['file1'], :formats => { :f1 => [nil, 'e1'] })).assets_with_format(:f1).collect{|a| a.absolute_path}.should == ["#{prefix}/c/file1.css", "#{prefix}/c/file1.e1.css" ]
|
85
|
+
end
|
86
|
+
|
87
|
+
it('should combine :extra_suffix with :optional_suffix') do
|
88
|
+
stub_fs([ '/c/file1.e.css', '/c/file1.e.css.o' ])
|
89
|
+
m(css_config(:files => ['file1'], :optional_suffix => 'o')).assets_with_extra_suffix('e').collect{|a| a.absolute_path}.should == [ "#{prefix}/c/file1.e.css.o" ]
|
90
|
+
end
|
91
|
+
|
92
|
+
it('should ignore too many dots when globbing') do
|
93
|
+
stub_fs([ '/c/file1.x.css' ])
|
94
|
+
m(css_config(:files => ['file1*'])).assets.collect{|a| a.absolute_path}.should == []
|
95
|
+
end
|
96
|
+
|
97
|
+
it('should pick files with :extra_suffix when globbing') do
|
98
|
+
stub_fs([ '/c/file1.e.css', '/c/file2.css' ])
|
99
|
+
m(css_config(:files => ['file*'])).assets_with_extra_suffix('e').collect{|a| a.absolute_path}.should == [ "#{prefix}/c/file1.e.css" ]
|
100
|
+
end
|
101
|
+
|
102
|
+
it('should pick files with :optional_suffix when globbing') do
|
103
|
+
stub_fs([ '/c/file.css', '/c/file.css.o' ])
|
104
|
+
m(css_config(:optional_suffix => 'o', :files => ['file*'])).assets.collect{|a| a.absolute_path}.should == [ "#{prefix}/c/file.css.o" ]
|
105
|
+
end
|
106
|
+
|
107
|
+
it('should pick files with both :extra_suffix and :optional_suffix when globbing') do
|
108
|
+
stub_fs([ '/c/file.css', '/c/file.e.css', '/c/file.e.css.o' ])
|
109
|
+
m(css_config(:optional_suffix => 'o', :files => ['file*'])).assets_with_extra_suffix('e').collect{|a| a.absolute_path}.should == [ "#{prefix}/c/file.e.css.o" ]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe('#cache_asset') do
|
114
|
+
it('should use options[:cache]') do
|
115
|
+
m(css_config).cache_asset.absolute_path.should == "#{prefix}/c/cache.css"
|
116
|
+
end
|
117
|
+
|
118
|
+
it('should use :format if set') do
|
119
|
+
m(css_config).cache_asset(:e).absolute_path.should == "#{prefix}/c/cache.e.css"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe('#each_compilation') do
|
124
|
+
it('should yield each set of input and output files') do
|
125
|
+
stub_fs([ '/c/file1.css', '/c/file1.e1.css', '/c/file2.css' ])
|
126
|
+
asset_module = m(css_config( :files => ['file1', 'file2'], :formats => { :f1 => [nil, 'e1'], :f2 => ['e1'] } ))
|
127
|
+
yields = asset_module.to_enum(:each_compilation).to_a
|
128
|
+
yields.should == [
|
129
|
+
[["#{prefix}/c/file1.css", "#{prefix}/c/file2.css"], "#{prefix}/c/cache.css"],
|
130
|
+
[["#{prefix}/c/file1.css", "#{prefix}/c/file2.css", "#{prefix}/c/file1.e1.css"], "#{prefix}/c/cache.f1.css"],
|
131
|
+
[["#{prefix}/c/file1.e1.css"], "#{prefix}/c/cache.f2.css"],
|
132
|
+
]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def m(config)
|
139
|
+
AssetLibrary::AssetModule.new(:name, config)
|
140
|
+
end
|
141
|
+
|
142
|
+
def js_config(options = {})
|
143
|
+
{
|
144
|
+
:cache => 'cache',
|
145
|
+
:base => 'j',
|
146
|
+
:suffix => 'js',
|
147
|
+
:files => [ 'file1', 'file2' ]
|
148
|
+
}.merge(options)
|
149
|
+
end
|
150
|
+
|
151
|
+
def css_config(options = {})
|
152
|
+
{
|
153
|
+
:cache => 'cache',
|
154
|
+
:base => 'c',
|
155
|
+
:suffix => 'css',
|
156
|
+
:files => [ 'file1', 'file2' ]
|
157
|
+
}.merge(options)
|
158
|
+
end
|
159
|
+
|
160
|
+
def prefix
|
161
|
+
@prefix ||= File.dirname(__FILE__) + '/deleteme'
|
162
|
+
end
|
163
|
+
|
164
|
+
def stub_fs(filenames)
|
165
|
+
wipe_fs
|
166
|
+
FileUtils.mkdir(prefix)
|
167
|
+
|
168
|
+
filenames.each do |file|
|
169
|
+
path = File.join(prefix, file)
|
170
|
+
dir = File.dirname(path)
|
171
|
+
unless File.exist?(dir)
|
172
|
+
FileUtils.mkdir_p(dir)
|
173
|
+
end
|
174
|
+
File.open(path, 'w') { |f| f.write("#{file}\n") }
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def wipe_fs
|
179
|
+
if File.exist?(prefix)
|
180
|
+
FileUtils.rm_r(prefix)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|