asset_library 0.4.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.rdoc ADDED
@@ -0,0 +1,172 @@
1
+ = asset_library
2
+
3
+ Bundles your JavaScript and CSS, so your development environment is simple to
4
+ code and your production environment is as fast as possible.
5
+
6
+ == Installation
7
+
8
+ First, install the gem:
9
+
10
+ sudo gem install alegscogs-asset_library --source http://gems.github.com
11
+
12
+ Next, add the gem into your Rails project's config/environment.rb:
13
+
14
+ config.gem 'alegscogs-asset_library', :lib => 'asset_library', :source => 'http://gems.github.com'
15
+
16
+ Finally, include the Rake tasks in your project:
17
+
18
+ echo "begin; require 'asset_library/rake_tasks'; rescue LoadError; end" > lib/tasks/asset_library.rake
19
+
20
+ == Usage
21
+
22
+ In your Rails project, edit <tt>config/asset_library.yml</tt> as described
23
+ in the following section.
24
+
25
+ Once configured, asset_library provides two helper methods for your views:
26
+
27
+ <%# outputs library.js (production) or its files (development) %>
28
+ <%= asset_library_javascript_tags(:javascripts) %>
29
+
30
+ <%# outputs library.css (production) or its files (development) %>
31
+ <%= asset_library_stylesheet_tags(:stylesheets) %>
32
+
33
+ <%# outputs library.ie6.css (production) or its files (development) %>
34
+ <!--[if lte IE 6]>
35
+ <%= asset_library_stylesheet_tags(:stylesheets, :ie6) %>
36
+ <![endif]-->
37
+
38
+ Both helpers behave differently depending on whether
39
+ <tt>ActionController::Base.perform_caching</tt> is true (that is, whether you
40
+ are in <tt>development</tt> environment or not). When caching is disabled, each
41
+ file in the module will be included. (Internet Explorer only allows 30
42
+ <tt>style</tt> and <tt>link</tt> stylesheet tags; in development mode,
43
+ <tt>import</tt> rules are used to work around the bug.) When caching is
44
+ enabled, a single tag is output.
45
+
46
+ When caching is enabled, the modules to include must be generated using:
47
+
48
+ rake asset_library:write
49
+
50
+ These moduels can be removed using:
51
+
52
+ rake asset_library:clean
53
+
54
+ A cached module is simply the concatenation of its constituent files.
55
+
56
+ == Configuration
57
+
58
+ A typical configuration (Yaml) file might look like this. In Rails, this
59
+ should be in <tt>config/asset_library.yml</tt>.
60
+
61
+ javascripts:
62
+ cache: library
63
+ optional_suffix: compressed
64
+ base: javascripts
65
+ suffix: js
66
+ files:
67
+ - vendor/jquery
68
+
69
+ # jQuery UI parts we need
70
+ - vendor/jquery-ui/ui.core
71
+ - vendor/jquery-ui/ui.draggable
72
+ - vendor/jquery-ui/ui.droppable
73
+ - vendor/jquery-ui/ui.sortable
74
+ - vendor/jquery-ui/effects.core
75
+
76
+ - lib
77
+ - plugins/*
78
+ - application
79
+ - initializers/*
80
+
81
+ tiny_mce_javascripts:
82
+ # TinyMCE doesn't give us a choice on cache name
83
+ cache: vendor/tiny_mce/tiny_mce_gzip
84
+ optional_suffix: compressed
85
+ base: javascripts
86
+ suffix: js
87
+ files:
88
+ - vendor/tiny_mce/tiny_mce
89
+ # ... it's possible to bundle all of TinyMCE together with a bit of magic
90
+
91
+ stylesheets:
92
+ cache: library
93
+ base: stylesheets
94
+ suffix: css
95
+ formats:
96
+ - ie6: [null, ie8, ie7, ie6]
97
+ - ie7: [null, ie8, ie7]
98
+ - ie8: [null, ie8]
99
+ files:
100
+ - reset
101
+ - application
102
+ - layout
103
+ - views/**/*
104
+
105
+ # in general...
106
+ #module_name:
107
+ # cache: cache_file
108
+ # base: base_path_of_assets_in_web_root
109
+ # suffix: suffix ("css" or "js")
110
+ # formats:
111
+ # format1: ["extra_suffix_1", "extra_suffix_2"]
112
+ # format2: [null, "extra_suffix3"]
113
+ # optional_suffix: optional_suffix
114
+ # files:
115
+ # - file1_relative_to_base
116
+ # - file2_relative_to_base
117
+ # - ...
118
+
119
+ Here are what the various configuration elements mean:
120
+
121
+ <tt>module_name</tt> is the name of the module. It is passed as the first
122
+ parameter to <tt>asset_library_javascript_tags</tt> or
123
+ <tt>asset_library_stylesheet_tags</tt>.
124
+
125
+ <tt>cache</tt> is a filename, without suffix, relative to <tt>base</tt>, where
126
+ the module will be bundled when caching is enabled. (Ensure that <tt>files</tt>
127
+ does not include <tt>cache_file</tt>, even with globbing.)
128
+
129
+ <tt>base</tt> is the base path of the assets in URLs. For instance, in Rails,
130
+ where stylesheets are usually served under <tt>/stylesheets</tt>, <tt>base</tt>
131
+ should be <tt>stylesheets</tt>.
132
+
133
+ <tt>suffix</tt> is either "js" or "css", depending on whether you are including
134
+ JavaScript or CSS files.
135
+
136
+ <tt>formats</tt> allows construction of parallel modules. By default, for a
137
+ module named <tt>styles</tt> will use <tt>*.css</tt> (where <tt>*</tt> is
138
+ specified by the <tt>files</tt> option) to generate <tt>styles.css</tt>; but
139
+ filenames of the format <tt>*.suffix.css</tt> will be ignored in that search.
140
+ If a <tt>format</tt> called <tt>format1</tt> is specified as
141
+ <tt>[suffix1, suffix2]</tt>, then <tt>*.suffix1.css</tt> and
142
+ <tt>*.suffix2.css</tt> will be included, but not <tt>*.css</tt>. (To specify
143
+ <tt>*.css</tt>, put <tt>null</tt> in the format list.)
144
+
145
+ <tt>optional_suffix</tt> will cause asset_library to check for the existence of
146
+ files with <tt>optional_suffix</tt> suffixes, falling back to files without
147
+ <tt>optional_suffix</tt> if necessary. For instance, if you run all your
148
+ JavaScript files through
149
+ {YUI Compressor}[http://developer.yahoo.com/yui/compressor/] and output the
150
+ compressed version of <tt>file1.js</tt> as <tt>file1.compressed.js</tt>, then
151
+ set <tt>optional_suffix</tt> to <tt>compressed</tt>. In your development
152
+ environment, where <tt>compressed</tt> javascripts are missing,
153
+ <tt>file1.js</tt> will be included and you can debug your JavaScript. In your
154
+ production environment, create the <tt>compressed</tt> JavaScripts in the same
155
+ directory, and they will be included instead, for optimal download speed.
156
+
157
+ <tt>files</tt> is a list of files, relative to <tt>base</tt>. File globbing is
158
+ allowed; <tt>**</tt> expands to "any nesting of directories". Files which do
159
+ not exist will be excluded; globbing will include/exclude files with
160
+ <tt>formats</tt> as appropriate; and files without <tt>optional_suffix</tt>
161
+ will not be output alongside files with the same name endowed with the suffix.
162
+
163
+ == Copyright
164
+
165
+ I believe in software freedom, not any abomination thereof. This project is
166
+ released under the Public Domain, meaning I relinquish my copyright (to any
167
+ extend the law allows) and grant you all rights to use, modify, sell, or
168
+ otherwise take advantage of my software.
169
+
170
+ However, I do kindly request that, as a favor, you refrain from using my
171
+ software as part of an evil plan involving velociraptors and mind-controlling
172
+ robots (even though I would not be legally entitled to sue you for doing so).
@@ -0,0 +1,35 @@
1
+ class AssetLibrary
2
+ class Asset
3
+ attr_reader(:absolute_path)
4
+
5
+ def initialize(absolute_path)
6
+ @absolute_path = absolute_path
7
+ end
8
+
9
+ def eql?(other)
10
+ self.class === other && absolute_path == other.absolute_path
11
+ end
12
+
13
+ def hash
14
+ self.absolute_path.hash
15
+ end
16
+
17
+ def relative_path
18
+ if AssetLibrary.root == '/'
19
+ absolute_path
20
+ else
21
+ absolute_path[AssetLibrary.root.length..-1]
22
+ end
23
+ end
24
+
25
+ def timestamp
26
+ File.mtime(absolute_path)
27
+ rescue Errno::ENOENT
28
+ nil
29
+ end
30
+
31
+ def relative_url
32
+ "#{relative_path}?#{timestamp.to_i.to_s}"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,84 @@
1
+ require File.dirname(__FILE__) + '/asset'
2
+
3
+ class AssetLibrary
4
+ class AssetModule
5
+ attr_reader(:config)
6
+
7
+ def initialize(config)
8
+ @config = config
9
+ end
10
+
11
+ # Returns an Array of Assets to include.
12
+ #
13
+ # Arguments:
14
+ # extra_suffix: if set, finds files with the given extra suffix
15
+ def assets(format = nil)
16
+ if format
17
+ assets_with_format(format)
18
+ else
19
+ assets_with_extra_suffix(nil)
20
+ end
21
+ end
22
+
23
+ # Returns an Array of Assets to include.
24
+ #
25
+ # Arguments:
26
+ # extra_suffix: if set, finds files with the given extra suffix
27
+ def assets_with_extra_suffix(extra_suffix)
28
+ return nil unless config
29
+
30
+ GlobFu.find(
31
+ config[:files],
32
+ :suffix => config[:suffix],
33
+ :extra_suffix => extra_suffix,
34
+ :root => File.join(*([AssetLibrary.root, config[:base]].compact)),
35
+ :optional_suffix => config[:optional_suffix]
36
+ ).collect { |f| Asset.new(f) }
37
+ end
38
+
39
+ # Returns an Array of Assets to include.
40
+ #
41
+ # Calls assets_with_extra_suffix for each suffix in the given format
42
+ #
43
+ # Arguments:
44
+ # format: format specified in the config
45
+ def assets_with_format(format)
46
+ return nil unless config
47
+
48
+ extra_suffixes = config[:formats][format.to_sym]
49
+ extra_suffixes.inject([]) { |r, s| r.concat(assets_with_extra_suffix(s)) }
50
+ end
51
+
52
+ def contents(format = nil)
53
+ s = StringIO.new
54
+
55
+ assets(format).each do |asset|
56
+ File.open(asset.absolute_path, 'r') do |infile|
57
+ s.write(infile.read)
58
+ end
59
+ end
60
+ s.rewind
61
+
62
+ s
63
+ end
64
+
65
+ # Returns an Asset representing the cache file
66
+ def cache_asset(format = nil)
67
+ extra = format ? ".#{format}" : ''
68
+ Asset.new(File.join(AssetLibrary.root, config[:base], "#{config[:cache]}#{extra}.#{config[:suffix]}"))
69
+ end
70
+
71
+ def write_cache(format = nil)
72
+ File.open(cache_asset(format).absolute_path, 'w') do |outfile|
73
+ outfile.write(contents(format).read)
74
+ end
75
+ end
76
+
77
+ def write_all_caches
78
+ write_cache
79
+ (config[:formats] || {}).keys.each do |format|
80
+ write_cache(format)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,96 @@
1
+ class AssetLibrary
2
+ module Helpers
3
+ def asset_library_javascript_tags(module_key, format = nil)
4
+
5
+ m = AssetLibrary.asset_module(module_key)
6
+ if AssetLibrary.cache
7
+ AssetLibrary.cache_vars[:javascript_tags] ||= {}
8
+ AssetLibrary.cache_vars[:javascript_tags][module_key] ||= asset_library_priv.script_tag(m.cache_asset)
9
+ else
10
+ m.assets(format).collect{|a| asset_library_priv.script_tag(a)}.join("\n")
11
+ end
12
+ end
13
+
14
+ def asset_library_stylesheet_tags(module_key, *args)
15
+ html_options = args.last.is_a?(Hash) ? args.pop : {}
16
+ format = args[0]
17
+
18
+ m = AssetLibrary.asset_module(module_key)
19
+ if AssetLibrary.cache
20
+ AssetLibrary.cache_vars[:stylesheet_tags] ||= {}
21
+ 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)
22
+ else
23
+ asset_library_priv.import_styles_tag(m.assets(format), html_options)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def asset_library_priv
30
+ @asset_library_priv ||= Priv.new(self)
31
+ end
32
+
33
+ class Priv
34
+ # Don't pollute helper's class's namespace with all our methods; put
35
+ # them here instead
36
+
37
+ attr_accessor :helper
38
+
39
+ def initialize(helper)
40
+ @helper = helper
41
+ end
42
+
43
+ def url(asset)
44
+ absolute_url(asset.relative_url)
45
+ end
46
+
47
+ def absolute_url(relative_url)
48
+ host = helper.__send__(:compute_asset_host, relative_url) if helper.respond_to?(:compute_asset_host, true)
49
+
50
+ host = nil if host == '' # Rails sets '' by default
51
+
52
+ if host && !(host =~ %r{^[-a-z]+://})
53
+ controller = helper.instance_variable_get(:@controller)
54
+ request = controller && controller.respond_to?(:request) && controller.request
55
+ host = request && "#{request.protocol}#{host}"
56
+ end
57
+
58
+ if host
59
+ "#{host}#{relative_url}"
60
+ else
61
+ relative_url
62
+ end
63
+ end
64
+
65
+ def script_tag(asset)
66
+ content_tag(:script, "", {:type => "text/javascript", :src => url(asset)})
67
+ end
68
+
69
+ def style_tag(asset, html_options = {})
70
+ "<link rel=\"stylesheet\" type=\"text/css\" href=\"#{url(asset)}\" #{attributes_from_hash(html_options)}/>"
71
+ end
72
+
73
+ def import_styles_tag(assets, html_options = {})
74
+ a = []
75
+ assets.each_slice(30) do |subset|
76
+ a << import_style_tag(subset, html_options)
77
+ end
78
+ a.join("\n")
79
+ end
80
+
81
+ def import_style_tag(assets, html_options = {})
82
+ imports = assets.collect{ |a| "@import \"#{url(a)}\";" }
83
+ content_tag(:style, "\n#{imports.join("\n")}\n", html_options.merge(:type => "text/css"))
84
+ end
85
+
86
+ def content_tag(name, content, options = {})
87
+ "<#{name} #{attributes_from_hash(options)}>#{content}</#{name}>"
88
+ end
89
+
90
+ def attributes_from_hash(options = {})
91
+ options.to_a.collect{|k, v| "#{k}=\"#{v}\""}.join(" ")
92
+ end
93
+
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,25 @@
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 = File.join(RAILS_ROOT, 'config', 'asset_library.yml')
6
+ AssetLibrary.root = File.join(RAILS_ROOT, 'public')
7
+ end
8
+
9
+ namespace(:asset_library) do
10
+ desc "Writes all asset caches specified in config/asset.yml by concatenating the constituent files."
11
+ task(:write) do
12
+ init_asset_library
13
+ AssetLibrary.write_all_caches
14
+ end
15
+
16
+ desc "Deletes all asset caches specified in config/asset.yml"
17
+ task(:clean) do
18
+ init_asset_library
19
+ keys = AssetLibrary.config.keys
20
+ asset_modules = keys.collect{|k| AssetLibrary.asset_module(k)}
21
+ asset_modules.each do |m|
22
+ FileUtils.rm_f(m.cache_asset.absolute_path)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ class AssetLibrary
2
+ module Util
3
+ def self.symbolize_hash_keys(hash)
4
+ return hash unless Hash === hash # because we recurse
5
+ hash.inject({}) do |ret, (key, value)|
6
+ ret[(key.to_sym rescue key) || key] = symbolize_hash_keys(value)
7
+ ret
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,71 @@
1
+ begin
2
+ require 'glob_fu'
3
+ rescue LoadError
4
+ require 'rubygems'
5
+ require 'glob_fu'
6
+ end
7
+
8
+ require File.dirname(__FILE__) + '/asset_library/asset_module'
9
+ require File.dirname(__FILE__) + '/asset_library/util'
10
+
11
+ class AssetLibrary
12
+ class << self
13
+ def config_path
14
+ @config_path
15
+ end
16
+
17
+ def config_path=(config_path)
18
+ @config_path = config_path
19
+ end
20
+
21
+ def root
22
+ @root
23
+ end
24
+
25
+ def root=(root)
26
+ @root = root
27
+ end
28
+
29
+ def cache
30
+ return true if @cache.nil?
31
+ @cache
32
+ end
33
+
34
+ def cache=(cache)
35
+ @config = nil
36
+ @cache_vars = nil
37
+ @cache = cache
38
+ end
39
+
40
+ def cache_vars
41
+ # We store cache_vars even if not caching--this is our "globals"
42
+ @cache_vars ||= {}
43
+ end
44
+
45
+ def config
46
+ return @config if cache && @config
47
+ ret = if File.exist?(config_path)
48
+ yaml = YAML.load_file(config_path) || {}
49
+ Util::symbolize_hash_keys(yaml)
50
+ else
51
+ {}
52
+ end
53
+ @config = cache ? ret : nil
54
+ ret
55
+ end
56
+
57
+ def asset_module(key)
58
+ module_config = config[key.to_sym]
59
+ if module_config
60
+ AssetModule.new(module_config)
61
+ end
62
+ end
63
+
64
+ def write_all_caches
65
+ config.keys.each do |key|
66
+ m = asset_module(key)
67
+ m.write_all_caches
68
+ end
69
+ end
70
+ end
71
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ AssetLibrary.cache = ActionController::Base.perform_caching
2
+ AssetLibrary.config_path = File.join(RAILS_ROOT, 'config', 'asset_library.yml')
3
+ AssetLibrary.root = File.join(RAILS_ROOT, 'public')
@@ -0,0 +1,216 @@
1
+ require 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('#contents') do
114
+ it('should return an IO object') do
115
+ stub_fs([ '/c/file1.css', '/c/file2.css' ])
116
+ m(css_config(:files => ['file*'])).contents.should(respond_to(:read))
117
+ end
118
+
119
+ it('should concatenate individual file contents') do
120
+ stub_fs([ '/c/file1.css', '/c/file2.css' ])
121
+ m(css_config(:files => ['file*'])).contents.read.should == "/c/file1.css\n/c/file2.css\n"
122
+ end
123
+ end
124
+
125
+ describe('#cache_asset') do
126
+ it('should use options[:cache]') do
127
+ m(css_config).cache_asset.absolute_path.should == "#{prefix}/c/cache.css"
128
+ end
129
+
130
+ it('should use :format if set') do
131
+ m(css_config).cache_asset(:e).absolute_path.should == "#{prefix}/c/cache.e.css"
132
+ end
133
+ end
134
+
135
+ describe('#write_cache') do
136
+ it('should write to cache.css') do
137
+ File.should_receive(:open).with("#{prefix}/c/cache.css", 'w')
138
+ m(css_config).write_cache
139
+ end
140
+
141
+ it('should write cache contents to cache') do
142
+ stub_fs([ '/c/file1.css', '/c/file2.css' ])
143
+ m(css_config(:files => ['file*'])).write_cache
144
+ File.open("#{prefix}/c/cache.css") { |f| f.read.should == "/c/file1.css\n/c/file2.css\n" }
145
+ end
146
+
147
+ it('should use :format to determine CSS output file') do
148
+ File.should_receive(:open).with("#{prefix}/c/cache.e.css", 'w')
149
+ m(css_config).write_cache(:e)
150
+ end
151
+ end
152
+
153
+ describe('#write_all_caches') do
154
+ it('should write cache.css (no :format)') do
155
+ File.should_receive(:open).with("#{prefix}/c/cache.css", 'w')
156
+ m(css_config).write_all_caches
157
+ end
158
+
159
+ it('should write no-format and all format files') do
160
+ formats = { :e1 => [], :e2 => [] }
161
+ File.should_receive(:open).with("#{prefix}/c/cache.css", 'w')
162
+ formats.keys.each do |format|
163
+ File.should_receive(:open).with("#{prefix}/c/cache.#{format}.css", 'w')
164
+ end
165
+ m(css_config(:formats => formats)).write_all_caches
166
+ end
167
+ end
168
+
169
+ private
170
+
171
+ def m(config)
172
+ AssetLibrary::AssetModule.new(config)
173
+ end
174
+
175
+ def js_config(options = {})
176
+ {
177
+ :cache => 'cache',
178
+ :base => 'j',
179
+ :suffix => 'js',
180
+ :files => [ 'file1', 'file2' ]
181
+ }.merge(options)
182
+ end
183
+
184
+ def css_config(options = {})
185
+ {
186
+ :cache => 'cache',
187
+ :base => 'c',
188
+ :suffix => 'css',
189
+ :files => [ 'file1', 'file2' ]
190
+ }.merge(options)
191
+ end
192
+
193
+ def prefix
194
+ @prefix ||= File.dirname(__FILE__) + '/deleteme'
195
+ end
196
+
197
+ def stub_fs(filenames)
198
+ wipe_fs
199
+ FileUtils.mkdir(prefix)
200
+
201
+ filenames.each do |file|
202
+ path = File.join(prefix, file)
203
+ dir = File.dirname(path)
204
+ unless File.exist?(dir)
205
+ FileUtils.mkdir_p(dir)
206
+ end
207
+ File.open(path, 'w') { |f| f.write("#{file}\n") }
208
+ end
209
+ end
210
+
211
+ def wipe_fs
212
+ if File.exist?(prefix)
213
+ FileUtils.rm_r(prefix)
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,49 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ require File.dirname(__FILE__) + '/../../lib/asset_library/asset'
4
+
5
+ describe(AssetLibrary::Asset) do
6
+ it('should have eql? work for identical assets') do
7
+ a('/a/b.css').should eql(a('/a/b.css'))
8
+ end
9
+
10
+ it('should use absolute_path as its hash') do
11
+ a('/a/b.css').hash.should == '/a/b.css'.hash
12
+ end
13
+
14
+ context('#relative_path') do
15
+ it('should strip AssetLibrary.root') do
16
+ AssetLibrary.stub!(:root).and_return('/r')
17
+ a('/r/a/b.css').relative_path.should == '/a/b.css'
18
+ end
19
+
20
+ it('should strip nothing if root is "/"') do
21
+ AssetLibrary.stub!(:root).and_return('/')
22
+ a('/r/a/b.css').relative_path.should == '/r/a/b.css'
23
+ end
24
+ end
25
+
26
+ context('#timestamp') do
27
+ it('should return the file mtime') do
28
+ File.stub!(:mtime).with('/r/a/b.css').and_return(Time.at(123))
29
+ a('/r/a/b.css').timestamp.should == Time.at(123)
30
+ end
31
+ end
32
+
33
+ context('#relative_url') do
34
+ before(:each) do
35
+ AssetLibrary.stub!(:root).and_return('/r')
36
+ end
37
+
38
+ it('should use relative path and mtime') do
39
+ File.stub!(:mtime).with('/r/a/b.css').and_return(Time.at(123))
40
+ a('/r/a/b.css').relative_url.should == '/a/b.css?123'
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def a(*args)
47
+ AssetLibrary::Asset.new(*args)
48
+ end
49
+ end
@@ -0,0 +1,210 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ require File.dirname(__FILE__) + '/../../lib/asset_library/helpers'
4
+
5
+ describe(AssetLibrary::Helpers) do
6
+ before(:each) do
7
+ @h = nil
8
+ AssetLibrary.stub!(:root).and_return('/')
9
+ end
10
+ before(:each) do
11
+ @old_cache = AssetLibrary.cache # Empty globals
12
+ end
13
+
14
+ after(:each) do
15
+ @h = nil
16
+ end
17
+ after(:each) do
18
+ AssetLibrary.cache = @old_cache
19
+ @old_cache = nil
20
+ end
21
+
22
+ describe('#asset_library_javascript_tags') do
23
+ describe('when not caching') do
24
+ before(:each) do
25
+ AssetLibrary.stub!(:cache).and_return(false)
26
+ end
27
+
28
+ it('should fetch using asset_module') do
29
+ m = mock(:assets => [])
30
+ AssetLibrary.should_receive(:asset_module).with(:m).and_return(m)
31
+ h.asset_library_javascript_tags(:m)
32
+ end
33
+
34
+ it('should output nothing when a module is empty') do
35
+ m = mock(:assets => [])
36
+ AssetLibrary.stub!(:asset_module).and_return(m)
37
+ h.asset_library_javascript_tags(:m).should == ''
38
+ end
39
+
40
+ it('should output a <script> tag for a file') do
41
+ m = mock(:assets => [a('/f.js')])
42
+ AssetLibrary.stub!(:asset_module).and_return(m)
43
+ h.asset_library_javascript_tags(:m).should == '<script type="text/javascript" src="/f.js?123"></script>'
44
+ end
45
+
46
+ it('should join <script> tags with newlines') do
47
+ m = mock(:assets => [a('/f.js'), a('/f2.js')])
48
+ AssetLibrary.stub!(:asset_module).and_return(m)
49
+ h.asset_library_javascript_tags(:m).should == '<script type="text/javascript" src="/f.js?123"></script>' + "\n" + '<script type="text/javascript" src="/f2.js?123"></script>'
50
+ end
51
+
52
+ it('should use compute_asset_host if available') do
53
+ m = mock(:assets => [a('/f.js')])
54
+ AssetLibrary.stub!(:asset_module).and_return(m)
55
+ h.should_receive(:compute_asset_host).with('/f.js?123').and_return('http://assets.test')
56
+ h.asset_library_javascript_tags(:m).should =~ %r{"http://assets.test/f.js\?123"}
57
+ end
58
+
59
+ it('should not use compute_asset_host if it returns nil') do
60
+ m = mock(:assets => [a('/f.js')])
61
+ AssetLibrary.stub!(:asset_module).and_return(m)
62
+ h.should_receive(:compute_asset_host).and_return(nil)
63
+ h.asset_library_javascript_tags(:m).should =~ %r{"/f.js\?123"}
64
+ end
65
+
66
+ it('should not use compute_asset_host if it returns ""') do
67
+ m = mock(:assets => [a('/f.js')])
68
+ AssetLibrary.stub!(:asset_module).and_return(m)
69
+ h.should_receive(:compute_asset_host).and_return("")
70
+ h.asset_library_javascript_tags(:m).should =~ %r{"/f.js\?123"}
71
+ end
72
+
73
+ it('should add request protocol to compute_asset_host output if applicable') do
74
+ m = mock(:assets => [a('/f.js')])
75
+ AssetLibrary.stub!(:asset_module).and_return(m)
76
+ h.stub!(:compute_asset_host).and_return('assets.test')
77
+ h.instance_variable_set(:@controller, mock(:request => mock(:protocol => 'http://')))
78
+ h.asset_library_javascript_tags(:m).should =~ %r{"http://assets.test/f.js\?123"}
79
+ end
80
+ end
81
+
82
+ describe('when caching') do
83
+ before(:each) do
84
+ AssetLibrary.cache = true
85
+ end
86
+
87
+ it('should output a single <script> tag with the cache filename') do
88
+ m = mock(:cache_asset => a('/cache.js'))
89
+ AssetLibrary.stub!(:asset_module).and_return(m)
90
+ h.asset_library_javascript_tags(:m).should == '<script type="text/javascript" src="/cache.js?123"></script>'
91
+ end
92
+
93
+ it('should use compute_asset_host if available') do
94
+ m = mock(:cache_asset => a('/cache.js'))
95
+ AssetLibrary.stub!(:asset_module).and_return(m)
96
+ h.should_receive(:compute_asset_host).with('/cache.js?123').and_return('http://assets.test')
97
+ h.asset_library_javascript_tags(:m)
98
+ h.asset_library_javascript_tags(:m).should =~ %r{"http://assets.test/cache.js\?123"}
99
+ end
100
+ end
101
+ end
102
+
103
+ describe('#asset_library_stylesheet_tags') do
104
+ describe('when not caching') do
105
+ before(:each) do
106
+ AssetLibrary.stub!(:cache).and_return(false)
107
+ end
108
+
109
+ it('should fetch using asset_module') do
110
+ m = mock(:assets => [])
111
+ AssetLibrary.should_receive(:asset_module).with(:m).and_return(m)
112
+ h.asset_library_stylesheet_tags(:m)
113
+ end
114
+
115
+ it('should output nothing when a module is empty') do
116
+ m = mock(:assets => [])
117
+ AssetLibrary.stub!(:asset_module).and_return(m)
118
+ h.asset_library_stylesheet_tags(:m).should == ''
119
+ end
120
+
121
+ it('should output a single <script> with a single @import when there is one file') do
122
+ m = mock(:assets => [a('/f.css')])
123
+ AssetLibrary.stub!(:asset_module).and_return(m)
124
+ h.asset_library_stylesheet_tags(:m).should == "<style type=\"text/css\">\n@import \"\/f.css?123\";\n</style>"
125
+ end
126
+
127
+ it('should use formats to find cache filename') do
128
+ m = mock
129
+ m.should_receive(:assets).with(:e).and_return([a('f.e.css')])
130
+ AssetLibrary.stub!(:asset_module).and_return(m)
131
+ h.asset_library_stylesheet_tags(:m, :e).should == "<style type=\"text/css\">\n@import \"f.e.css?123\";\n</style>"
132
+ end
133
+
134
+ it('should output a single <script> tag with 30 @import') do
135
+ m = mock(:assets => (1..30).collect{|i| a("/f#{i}.css") })
136
+ AssetLibrary.stub!(:asset_module).and_return(m)
137
+ h.asset_library_stylesheet_tags(:m).should =~ /\<style type=\"text\/css\"\>(\n@import \"\/f\d+.css\?123\";){30}\n\<\/style\>/
138
+ end
139
+
140
+ it('should output two <script> tags with 31 @imports') do
141
+ m = mock(:assets => (1..31).collect{|i| a("/f#{i}.css") })
142
+ AssetLibrary.stub!(:asset_module).and_return(m)
143
+ h.asset_library_stylesheet_tags(:m).should =~ /\<style type="text\/css"\>(\n@import "\/f\d+.css\?123";){30}\n\<\/style\>\n<style type="text\/css"\>\n@import "\/f31.css\?123";\n\<\/style\>/
144
+ end
145
+
146
+ it('should output a final hash in the parameters as html attributes') do
147
+ m = mock(:assets => [a('/f.css')])
148
+ AssetLibrary.stub!(:asset_module).and_return(m)
149
+ optional_hash = {:key1 => "val1", :key2 => "val2", :key3 => "val3"}
150
+ attributes_to_hash( h.asset_library_stylesheet_tags(:m, optional_hash), [:type] ).should == optional_hash
151
+ end
152
+ end
153
+
154
+ describe('when caching') do
155
+ before(:each) do
156
+ AssetLibrary.stub!(:cache).and_return(true)
157
+ end
158
+
159
+ it('should output a single <style> tag with the cache filename') do
160
+ m = mock(:cache_asset => a('/cache.css'))
161
+ AssetLibrary.stub!(:asset_module).and_return(m)
162
+ h.asset_library_stylesheet_tags(:m).should == '<link rel="stylesheet" type="text/css" href="/cache.css?123" />'
163
+ end
164
+
165
+ it('should use format for the cache filename') do
166
+ m = mock
167
+ m.should_receive(:cache_asset).with(:e).and_return(a('/cache.e.css'))
168
+ AssetLibrary.stub!(:asset_module).and_return(m)
169
+ h.asset_library_stylesheet_tags(:m, :e).should == '<link rel="stylesheet" type="text/css" href="/cache.e.css?123" />'
170
+ end
171
+
172
+ it('should output a final hash in the parameters as html attributes') do
173
+ m = mock(:cache_asset => a('/cache.css'))
174
+ AssetLibrary.stub!(:asset_module).and_return(m)
175
+ optional_hash = {:key1 => "val1", :key2 => "val2", :key3 => "val3"}
176
+ attributes_to_hash(h.asset_library_stylesheet_tags(:m, optional_hash), [:type, :rel, :href]).should == optional_hash
177
+ end
178
+ end
179
+ end
180
+
181
+ private
182
+
183
+ def a(path)
184
+ File.stub!(:mtime).and_return(Time.at(123))
185
+ AssetLibrary::Asset.new(path)
186
+ end
187
+
188
+ def h
189
+ return @h if @h
190
+ c = Class.new do
191
+ include AssetLibrary::Helpers
192
+ end
193
+ @h = c.new
194
+ @h.stub!(:request).and_return(mock(:protocol => 'http://', :host_with_port => 'example.com'))
195
+ @h
196
+ end
197
+
198
+ def attributes_to_hash(string, without = [])
199
+ hash_from_tag_attributes = {}
200
+
201
+ string.scan(/\s([^\s=]+="[^"]*)"/).each do |attr|
202
+ a = attr[0].split("=\"")
203
+ hash_from_tag_attributes.merge!( a[0].to_sym => a[1] )
204
+ end
205
+
206
+ without.each{|k| hash_from_tag_attributes.delete k}
207
+
208
+ hash_from_tag_attributes
209
+ end
210
+ end
@@ -0,0 +1,97 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe(AssetLibrary) do
4
+ before(:each) do
5
+ @old_root = AssetLibrary.root
6
+ @old_config_path = AssetLibrary.config_path
7
+ @old_cache = AssetLibrary.cache
8
+ end
9
+
10
+ after(:each) do
11
+ AssetLibrary.root = @old_root
12
+ AssetLibrary.config_path = @old_config_path
13
+ AssetLibrary.cache = @old_cache
14
+ end
15
+
16
+ describe('#config') do
17
+ it('should YAML.load_file the config from config_path') do
18
+ AssetLibrary.config_path = '/config.yml'
19
+ File.stub!(:exist?).with('/config.yml').and_return(true)
20
+ YAML.should_receive(:load_file).with('/config.yml')
21
+ AssetLibrary.config
22
+ end
23
+
24
+ it('should return {} if config_path does not exist') do
25
+ AssetLibrary.config_path = '/config.yml'
26
+ File.stub!(:exist?).with('/config.yml').and_return(false)
27
+ AssetLibrary.config.should == {}
28
+ end
29
+
30
+ it('should cache config if cache is set') do
31
+ AssetLibrary.cache = true
32
+ AssetLibrary.config_path = '/config.yml'
33
+
34
+ File.stub!(:exist?).with('/config.yml').and_return(true)
35
+ YAML.should_receive(:load_file).with('/config.yml').once
36
+
37
+ AssetLibrary.config
38
+ AssetLibrary.config
39
+ end
40
+
41
+ it('should not cache config if cache is not set') do
42
+ AssetLibrary.cache = false
43
+ AssetLibrary.config_path = '/config.yml'
44
+
45
+ File.stub!(:exist?).with('/config.yml').and_return(true)
46
+ YAML.should_receive(:load_file).with('/config.yml').twice
47
+
48
+ AssetLibrary.config
49
+ AssetLibrary.config
50
+ end
51
+
52
+ it('should symbolize config hash keys') do
53
+ AssetLibrary.cache = false
54
+ AssetLibrary.config_path = '/config.yml'
55
+
56
+ File.stub!(:exist?).with('/config.yml').and_return(true)
57
+ YAML.should_receive(:load_file).with('/config.yml').and_return(
58
+ { 'a' => { 'b' => 'c' } }
59
+ )
60
+
61
+ AssetLibrary.config.should == { :a => { :b => 'c' } }
62
+ end
63
+ end
64
+
65
+ describe('#asset_module') do
66
+ before(:each) do
67
+ @config = {}
68
+ AssetLibrary.stub!(:config).and_return(@config)
69
+ end
70
+
71
+ it('should return nil when given an invalid key') do
72
+ AssetLibrary.asset_module(:foo).should == nil
73
+ end
74
+
75
+ it('should return an AssetModule when given a valid key') do
76
+ @config[:foo] = {}
77
+ AssetLibrary.asset_module(:foo).should(be_a(AssetLibrary::AssetModule))
78
+ end
79
+ end
80
+
81
+ describe('#write_all_caches') do
82
+ it('should call write_all_caches on all asset_modules') do
83
+ mock1 = mock
84
+ mock2 = mock
85
+
86
+ mock1.should_receive(:write_all_caches)
87
+ mock2.should_receive(:write_all_caches)
88
+
89
+ AssetLibrary.stub!(:asset_module).with(:mock1).and_return(mock1)
90
+ AssetLibrary.stub!(:asset_module).with(:mock2).and_return(mock2)
91
+
92
+ AssetLibrary.stub!(:config).and_return({ :mock1 => {}, :mock2 => {} })
93
+
94
+ AssetLibrary.write_all_caches
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+
4
+ require File.dirname(__FILE__) + '/../lib/asset_library'
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: asset_library
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - adamh
8
+ - alegscogs
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2010-02-09 00:00:00 -05:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: adamh-glob_fu
18
+ type: :runtime
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: 0.0.4
25
+ version:
26
+ description:
27
+ email: adam@adamhooper.com
28
+ executables: []
29
+
30
+ extensions: []
31
+
32
+ extra_rdoc_files:
33
+ - README.rdoc
34
+ files:
35
+ - lib/asset_library.rb
36
+ - lib/asset_library/asset.rb
37
+ - lib/asset_library/asset_module.rb
38
+ - lib/asset_library/helpers.rb
39
+ - lib/asset_library/rake_tasks.rb
40
+ - lib/asset_library/util.rb
41
+ - rails/init.rb
42
+ - README.rdoc
43
+ has_rdoc: true
44
+ homepage: http://github.com/adamh/asset_library
45
+ licenses: []
46
+
47
+ post_install_message:
48
+ rdoc_options:
49
+ - --charset=UTF-8
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ requirements: []
65
+
66
+ rubyforge_project:
67
+ rubygems_version: 1.3.5
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: Manage and bundle CSS and JavaScript files
71
+ test_files:
72
+ - spec/asset_library_spec.rb
73
+ - spec/asset_library/asset_module_spec.rb
74
+ - spec/asset_library/asset_spec.rb
75
+ - spec/asset_library/helpers_spec.rb
76
+ - spec/spec_helper.rb