adamh-asset_library 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,163 @@
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 adamh-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 'adamh-asset_library', :lib => 'asset_library', :source => 'http://gems.github.com'
15
+
16
+ Finally, include the Rake tasks in your project:
17
+
18
+ echo "require 'asset_library/rake_tasks'" > lib/tasks/asset_library.rake
19
+
20
+ == Usage
21
+
22
+ Two methods are made available for your views:
23
+
24
+ <% # outputs library.js (production) or its files (development) %>
25
+ <%= asset_library_javascript_tags(:library) %>
26
+
27
+ <% # outputs library.css (production) or its files (development) %>
28
+ <%= asset_library_stylesheet_tags(:library) %>
29
+
30
+ <% # outputs library.ie6.css (production) or its files (development) %>
31
+ <!--[if lte IE 6]>
32
+ <%= asset_library_stylesheet_tags(:library, 'ie6') %>
33
+ <![endif]-->
34
+
35
+ Both helpers behave differently depending on whether
36
+ <tt>ActionController::Base.perform_caching</tt> is true (that is, whether you are in
37
+ <tt>development</tt> environment or not). When caching is disabled, each file in the
38
+ module will be included. (Internet Explorer only allows 30 <tt>style</tt> and <tt>link</tt>
39
+ stylesheet tags; in development mode, <tt>import</tt> rules are used to work around
40
+ the bug.) When caching is enabled, a single tag is output.
41
+
42
+ When caching is enabled, the modules to include must be generated using:
43
+
44
+ rake asset_library:write
45
+
46
+ These moduels can be removed using:
47
+
48
+ rake asset_library:clean
49
+
50
+ A cached module is simply the concatenation of its constituent files.
51
+
52
+ == Configuration
53
+
54
+ A typical configuration (Yaml) file might look like this:
55
+
56
+ javascripts:
57
+ cache: library
58
+ optional_suffix: compressed
59
+ base: javascripts
60
+ suffix: js
61
+ files:
62
+ - vendor/jquery
63
+
64
+ # jQuery UI parts we need
65
+ - vendor/jquery-ui/ui.core
66
+ - vendor/jquery-ui/ui.draggable
67
+ - vendor/jquery-ui/ui.droppable
68
+ - vendor/jquery-ui/ui.sortable
69
+ - vendor/jquery-ui/effects.core
70
+
71
+ - lib
72
+ - plugins/*
73
+ - application
74
+ - initializers/*
75
+
76
+ tiny_mce_javascripts:
77
+ # TinyMCE doesn't give us a choice on cache name
78
+ cache: vendor/tiny_mce/tiny_mce_gzip
79
+ optional_suffix: compressed
80
+ base: javascripts
81
+ suffix: js
82
+ files:
83
+ - vendor/tiny_mce/tiny_mce
84
+ # ... it's possible to bundle all of TinyMCE together with a bit of magic
85
+
86
+ stylesheets:
87
+ cache: library
88
+ base: stylesheets
89
+ suffix: css
90
+ extra_suffixes: [ie6, ie7, ie8]
91
+ files:
92
+ - reset
93
+ - application
94
+ - layout
95
+ - views/**/*
96
+
97
+ # in general...
98
+ #module_name:
99
+ # cache: cache_file
100
+ # base: base_path_of_assets_in_web_root
101
+ # suffix: suffix ("css" or "js")
102
+ # extra_suffixes: ["other_bundle_suffix_1", "other_bundle_suffix_2"]
103
+ # optional_suffix: optional_suffix
104
+ # files:
105
+ # - file1_relative_to_base
106
+ # - file2_relative_to_base
107
+ # - ...
108
+
109
+ Here are what the various configuration elements mean:
110
+
111
+ <tt>module_name</tt> is the name of the module. It is passed as the first
112
+ parameter to <tt>asset_library_javascript_tags</tt> or
113
+ <tt>asset_library_stylesheet_tags</tt>.
114
+
115
+ <tt>cache</tt> is a filename, without suffix, relative to <tt>base</tt>, where
116
+ the module will be bundled when caching is enabled. (Ensure that <tt>files</tt>
117
+ does not include <tt>cache_file</tt>, even with globbing.)
118
+
119
+ <tt>base</tt> is the base path of the assets in URLs. For instance, in Rails,
120
+ where stylesheets are usually served under <tt>/stylesheets</tt>, <tt>base</tt>
121
+ should be <tt>stylesheets</tt>.
122
+
123
+ <tt>suffix</tt> is either "js" or "css", depending on whether you are including
124
+ JavaScript or CSS files.
125
+
126
+ <tt>extra_suffixes</tt> allows construction of parallel modules. If you specify
127
+ <tt>extra_suffixes</tt> as ['ie6', 'ie7'], <tt>files</tt> as <tt>file1</tt> and
128
+ <tt>file2</tt>, <tt>module_name</tt> as <tt>module</tt>, and <tt>suffix</tt> as
129
+ <tt>css</tt>, then three modules will be created (ignoring nonexistent files):
130
+
131
+ * <tt>module.css</tt>, the concatenation of <tt>file1.css</tt> and <tt>file2.css</tt>
132
+ * <tt>module.ie6.css</tt>, the concatenation of <tt>file1.ie6.css</tt> and <tt>file2.ie6.css</tt>
133
+ * <tt>module.ie7.css</tt>, the concatenation of <tt>file1.ie7.css</tt> and <tt>file2.ie7.css</tt>
134
+
135
+ <tt>optional_suffix</tt> will cause asset_library to check for the existence of
136
+ files with <tt>optional_suffix</tt> suffixes, falling back to files without
137
+ <tt>optional_suffix</tt> if necessary. For instance, if you run all your
138
+ JavaScript files through
139
+ {YUI Compressor}[http://developer.yahoo.com/yui/compressor/] and output the
140
+ compressed version of <tt>file1.js</tt> as <tt>file1.compressed.js</tt>, then
141
+ set <tt>optional_suffix</tt> to <tt>compressed</tt>. In your development
142
+ environment, where <tt>compressed</tt> javascripts are missing,
143
+ <tt>file1.js</tt> will be included and you can debug your JavaScript. In your
144
+ production environment, create the <tt>compressed</tt> JavaScripts in the same
145
+ directory, and they will be included instead, for optimal download speed.
146
+
147
+ <tt>files</tt> is a list of files, relative to <tt>base</tt>. File globbing is
148
+ allowed; <tt>**</tt> expands to "any nesting of directories". Files which do
149
+ not exist will be excluded; globbing will include/exclude files with
150
+ <tt>extra_suffixes</tt> as appropriate; and files without
151
+ <tt>optional_suffix</tt> will not be output alongside files with the same name
152
+ but lacking the suffix.
153
+
154
+ == Copyright
155
+
156
+ I believe in software freedom, not any abomination thereof. This project is
157
+ released under the Public Domain, meaning I relinquish my copyright (to any
158
+ extend the law allows) and grant you all rights to use, modify, sell, or
159
+ otherwise take advantage of my software.
160
+
161
+ However, I do kindly request that, as a favor, you refrain from using my
162
+ software as part of an evil plan involving velociraptors and mind-controlling
163
+ robots (even though I would not be legally entitled to sue you for doing so).
@@ -0,0 +1,58 @@
1
+ require File.dirname(__FILE__) + '/asset_library/asset_module'
2
+ require File.dirname(__FILE__) + '/asset_library/util'
3
+
4
+ class AssetLibrary
5
+ class << self
6
+ def config_path
7
+ @config_path
8
+ end
9
+
10
+ def config_path=(config_path)
11
+ @config_path = config_path
12
+ end
13
+
14
+ def root
15
+ @root
16
+ end
17
+
18
+ def root=(root)
19
+ @root = root
20
+ end
21
+
22
+ def cache
23
+ return true if @cache.nil?
24
+ @cache
25
+ end
26
+
27
+ def cache=(cache)
28
+ @config = nil
29
+ @cache = cache
30
+ end
31
+
32
+ def config
33
+ return @config if cache && @config
34
+ ret = if File.exist?(config_path)
35
+ yaml = YAML.load_file(config_path) || {}
36
+ Util::symbolize_hash_keys(yaml)
37
+ else
38
+ {}
39
+ end
40
+ @config = cache ? ret : nil
41
+ ret
42
+ end
43
+
44
+ def asset_module(key)
45
+ module_config = config[key.to_sym]
46
+ if module_config
47
+ AssetModule.new(module_config)
48
+ end
49
+ end
50
+
51
+ def write_all_caches
52
+ config.keys.each do |key|
53
+ m = asset_module(key)
54
+ m.write_all_caches
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,33 @@
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
+ end
28
+
29
+ def relative_url
30
+ "#{relative_path}?#{timestamp.to_i.to_s}"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,105 @@
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(extra_suffix = nil)
16
+ return nil unless config
17
+
18
+ ret = []
19
+ config[:files].each do |requested_file|
20
+ ret.concat(assets_for_pattern(requested_file, extra_suffix))
21
+ end
22
+ ret.uniq!
23
+ ret
24
+ end
25
+
26
+ def contents(extra_suffix = nil)
27
+ s = StringIO.new
28
+
29
+ assets(extra_suffix).each do |asset|
30
+ File.open(asset.absolute_path, 'r') do |infile|
31
+ s.write(infile.read)
32
+ end
33
+ end
34
+ s.rewind
35
+
36
+ s
37
+ end
38
+
39
+ # Returns an Asset representing the cache file
40
+ def cache_asset(extra_suffix = nil)
41
+ extra = extra_suffix ? ".#{extra_suffix}" : ''
42
+ Asset.new(File.join(AssetLibrary.root, config[:base], "#{config[:cache]}#{extra}.#{config[:suffix]}"))
43
+ end
44
+
45
+ def write_cache(extra_suffix = nil)
46
+ File.open(cache_asset(extra_suffix).absolute_path, 'w') do |outfile|
47
+ outfile.write(contents(extra_suffix).read)
48
+ end
49
+ end
50
+
51
+ def write_all_caches
52
+ cache_suffixes = [ nil ] + (config[:extra_suffixes] || [])
53
+ cache_suffixes.each do |extra_suffix|
54
+ write_cache(extra_suffix)
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def assets_for_pattern(pattern, extra_suffix)
61
+ ret = []
62
+
63
+ suffix = config[:suffix]
64
+ suffix = "#{extra_suffix}.#{suffix}" if extra_suffix
65
+
66
+ requested_path = File.join(AssetLibrary.root, config[:base], "#{pattern}.#{suffix}")
67
+
68
+ Dir.glob(requested_path).sort.each do |found_file|
69
+ found_file = maybe_add_optional_suffix_to_path(found_file)
70
+ next if path_contains_extra_dot?(found_file, pattern, extra_suffix)
71
+ ret << AssetLibrary::Asset.new(found_file)
72
+ end
73
+
74
+ ret
75
+ end
76
+
77
+ def maybe_add_optional_suffix_to_path(path)
78
+ if config[:optional_suffix]
79
+ basename = path[0..-(config[:suffix].length + 2)]
80
+ path_with_suffix = "#{basename}.#{config[:optional_suffix]}.#{config[:suffix]}"
81
+ File.exist?(path_with_suffix) ? path_with_suffix : path
82
+ else
83
+ path
84
+ end
85
+ end
86
+
87
+ def path_contains_extra_dot?(path, requested_file, extra_suffix)
88
+ allowed_suffixes = []
89
+
90
+ allowed_suffixes << "\\.#{Regexp.quote(extra_suffix)}" if extra_suffix
91
+ allowed_suffixes << "(\\.#{Regexp.quote(config[:optional_suffix])})?" if config[:optional_suffix]
92
+ allowed_suffixes << "\\.#{Regexp.quote(config[:suffix])}" if config[:suffix]
93
+
94
+ basename = File.basename(path)
95
+ requested_basename = File.basename(requested_file)
96
+
97
+ n_dots = requested_basename.count('.')
98
+ basename_regex = (['[^\.]+'] * (n_dots + 1)).join('\.')
99
+
100
+ regex = /^#{basename_regex}#{allowed_suffixes.join}$/
101
+
102
+ !(basename =~ regex)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,40 @@
1
+ class AssetLibrary
2
+ module Helpers
3
+ def asset_library_javascript_tags(module_key)
4
+ m = AssetLibrary.asset_module(module_key)
5
+ if AssetLibrary.cache
6
+ script_tag(m.cache_asset.relative_url)
7
+ else
8
+ m.assets.collect{|a| script_tag(a.relative_url)}.join("\n")
9
+ end
10
+ end
11
+
12
+ def asset_library_stylesheet_tags(module_key, extra_suffix = nil)
13
+ m = AssetLibrary.asset_module(module_key)
14
+ if AssetLibrary.cache
15
+ style_tag(m.cache_asset(extra_suffix).relative_url)
16
+ else
17
+ import_styles_tag(m.assets(extra_suffix).collect{|a| a.relative_url})
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def script_tag(url)
24
+ "<script type=\"text/javascript\" src=\"#{url}\"></script>"
25
+ end
26
+
27
+ def style_tag(url)
28
+ "<link rel=\"stylesheet\" type=\"text/css\" href=\"#{url}\" />"
29
+ end
30
+
31
+ def import_styles_tag(urls)
32
+ urls.each_slice(30).collect{ |subset| import_style_tag(subset) }.join("\n")
33
+ end
34
+
35
+ def import_style_tag(urls)
36
+ imports = urls.collect{ |u| "@import \"#{u}\";" }
37
+ "<style type=\"text/css\">\n#{imports.join("\n")}\n</style>"
38
+ end
39
+ end
40
+ 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
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,193 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ require 'set'
4
+ require 'rglob'
5
+
6
+ describe(AssetLibrary::AssetModule) do
7
+ before(:each) do
8
+ AssetLibrary.stub!(:root).and_return('/')
9
+ end
10
+
11
+ describe('#assets') do
12
+ it('should include file1 and file2') do
13
+ files = [ '/c/file1.css', '/c/file2.css' ]
14
+ stub_fs(files)
15
+ m(css_config(:files => ['file1', 'file2'])).assets.collect{|a| a.absolute_path}.should == files
16
+ end
17
+
18
+ it('should not include file2 if that does not exist') do
19
+ files = [ '/c/file1.css' ]
20
+ stub_fs(files)
21
+ m(css_config(:files => ['file1', 'file2'])).assets.collect{|a| a.absolute_path}.should == files
22
+ end
23
+
24
+ it('should not include other files') do
25
+ files = [ '/c/file1.css', '/c/file2.css' ]
26
+ stub_fs(files)
27
+ m(css_config(:files => ['file1'])).assets.collect{|a| a.absolute_path}.should == [ files.first ]
28
+ end
29
+
30
+ it('should glob filenames') do
31
+ files = [ '/c/file1.css', '/c/file2.css', '/c/other_file.css' ]
32
+ stub_fs(files)
33
+ m(css_config(:files => ['file*'])).assets.collect{|a| a.absolute_path}.should == files[0..1]
34
+ end
35
+
36
+ it('should glob directories') do
37
+ files = [ '/c/file1.css', '/c/a/file2.css', '/c/b/a/file3.css' ]
38
+ stub_fs(files)
39
+ m(css_config(:files => ['**/file*'])).assets.collect{|a| a.absolute_path}.should == files[1..2]
40
+ end
41
+
42
+ it('should use :optional_suffix when appropriate') do
43
+ files = [ '/c/file1.css', '/c/file1.o.css' ]
44
+ stub_fs(files)
45
+ m(css_config(:optional_suffix => 'o', :files => ['file1'])).assets.collect{|a| a.absolute_path}.should == files[1..1]
46
+ end
47
+
48
+ it('should not show :optional_suffix file if original is absent') do
49
+ files = [ '/c/file1.o.css' ]
50
+ stub_fs(files)
51
+ m(css_config(:optional_suffix => 'o', :files => ['file1'])).assets.collect{|a| a.absolute_path}.should == []
52
+ end
53
+
54
+ it('should ignore :optional_suffix when suffixed file is not present') do
55
+ stub_fs([ '/c/file1.css' ])
56
+ m(css_config(:optional_suffix => 'o', :files => ['file1'])).assets.collect{|a| a.absolute_path}.should == [ '/c/file1.css' ]
57
+ end
58
+
59
+ it('should pick files with :extra_suffix') do
60
+ stub_fs([ '/c/file1.e.css' ])
61
+ m(css_config(:files => ['file1'])).assets('e').collect{|a| a.absolute_path}.should == [ '/c/file1.e.css' ]
62
+ end
63
+
64
+ it('should ignore non-suffixed files when :extra_suffix is set') do
65
+ stub_fs([ '/c/file1.css' ])
66
+ m(css_config(:files => ['file1'])).assets('e').collect{|a| a.absolute_path}.should == []
67
+ end
68
+
69
+ it('should combine :extra_suffix with :optional_suffix') do
70
+ stub_fs([ '/c/file1.e.css', '/c/file1.e.o.css' ])
71
+ m(css_config(:files => ['file1'], :optional_suffix => 'o')).assets('e').collect{|a| a.absolute_path}.should == [ '/c/file1.e.o.css' ]
72
+ end
73
+
74
+ it('should ignore too many dots when globbing') do
75
+ stub_fs([ '/c/file1.x.css' ])
76
+ m(css_config(:files => ['file1*'])).assets.collect{|a| a.absolute_path}.should == []
77
+ end
78
+
79
+ it('should pick files with :extra_suffix when globbing') do
80
+ stub_fs([ '/c/file1.e.css', '/c/file2.css' ])
81
+ m(css_config(:files => ['file*'])).assets('e').collect{|a| a.absolute_path}.should == [ '/c/file1.e.css' ]
82
+ end
83
+
84
+ it('should pick files with :optional_suffix when globbing') do
85
+ stub_fs([ '/c/file.css', '/c/file.o.css' ])
86
+ m(css_config(:optional_suffix => 'o', :files => ['file*'])).assets.collect{|a| a.absolute_path}.should == [ '/c/file.o.css' ]
87
+ end
88
+
89
+ it('should pick files with both :extra_suffix and :optional_suffix when globbing') do
90
+ stub_fs([ '/c/file.css', '/c/file.e.css', '/c/file.e.o.css' ])
91
+ m(css_config(:optional_suffix => 'o', :files => ['file*'])).assets('e').collect{|a| a.absolute_path}.should == [ '/c/file.e.o.css' ]
92
+ end
93
+ end
94
+
95
+ describe('#contents') do
96
+ it('should return an IO object') do
97
+ stub_fs([ '/c/file1.css', '/c/file2.css' ])
98
+ m(css_config(:files => ['file*'])).contents.should(respond_to(:read))
99
+ end
100
+
101
+ it('should concatenate individual file contents') do
102
+ stub_fs([ '/c/file1.css', '/c/file2.css' ])
103
+ m(css_config(:files => ['file*'])).contents.read.should == '/c/file1.css/c/file2.css'
104
+ end
105
+ end
106
+
107
+ describe('#cache_asset') do
108
+ it('should use options[:cache]') do
109
+ m(css_config).cache_asset.absolute_path.should == '/c/cache.css'
110
+ end
111
+
112
+ it('should use :extra_suffix if set') do
113
+ m(css_config).cache_asset('e').absolute_path.should == '/c/cache.e.css'
114
+ end
115
+ end
116
+
117
+ describe('#write_cache') do
118
+ it('should write to cache.css') do
119
+ File.should_receive(:open).with('/c/cache.css', 'w')
120
+ m(css_config).write_cache
121
+ end
122
+
123
+ it('should write cache contents to cache') do
124
+ f = StringIO.new
125
+ File.stub!(:open).with('/c/cache.css', 'w').and_yield(f)
126
+ stub_fs([ '/c/file1.css', '/c/file2.css' ])
127
+ m(css_config(:files => ['file*'])).write_cache
128
+ f.rewind
129
+ f.read.should == '/c/file1.css/c/file2.css'
130
+ end
131
+
132
+ it('should use :extra_suffix to determine CSS output file') do
133
+ File.should_receive(:open).with('/c/cache.e.css', 'w')
134
+ m(css_config).write_cache('e')
135
+ end
136
+ end
137
+
138
+ describe('#write_all_caches') do
139
+ it('should write cache.css (no :extra_suffix)') do
140
+ File.should_receive(:open).with('/c/cache.css', 'w')
141
+ m(css_config).write_all_caches
142
+ end
143
+
144
+ it('should write no-extra_suffix and all extra_suffix files') do
145
+ suffixes = [ 'e1', 'e2' ]
146
+ File.should_receive(:open).with('/c/cache.css', 'w')
147
+ suffixes.each do |suffix|
148
+ File.should_receive(:open).with("/c/cache.#{suffix}.css", 'w')
149
+ end
150
+ m(css_config(:extra_suffixes => suffixes)).write_all_caches
151
+ end
152
+ end
153
+
154
+ private
155
+
156
+ def m(config)
157
+ AssetLibrary::AssetModule.new(config)
158
+ end
159
+
160
+ def js_config(options = {})
161
+ {
162
+ :cache => 'cache',
163
+ :base => 'j',
164
+ :suffix => 'js',
165
+ :files => [ 'file1', 'file2' ]
166
+ }.merge(options)
167
+ end
168
+
169
+ def css_config(options = {})
170
+ {
171
+ :cache => 'cache',
172
+ :base => 'c',
173
+ :suffix => 'css',
174
+ :files => [ 'file1', 'file2' ]
175
+ }.merge(options)
176
+ end
177
+
178
+ def stub_fs(filenames)
179
+ filenames = Set.new(filenames)
180
+ File.stub!(:exist?).and_return do |path|
181
+ filenames.include?(path)
182
+ end
183
+
184
+ filenames.each do |path|
185
+ File.stub!(:open).with(path, 'r').and_yield(StringIO.new(path))
186
+ end
187
+
188
+ Dir.stub!(:glob).and_return do |path|
189
+ glob = RGlob::Glob.new(path)
190
+ filenames.select { |f| glob.match(f) }
191
+ end
192
+ end
193
+ 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,131 @@
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
+ AssetLibrary.stub!(:root).and_return('/')
8
+ end
9
+
10
+ describe('#asset_library_javascript_tags') do
11
+ describe('when not caching') do
12
+ before(:each) do
13
+ AssetLibrary.stub!(:cache).and_return(false)
14
+ end
15
+
16
+ it('should fetch using asset_module') do
17
+ m = mock(:assets => [])
18
+ AssetLibrary.should_receive(:asset_module).with(:m).and_return(m)
19
+ h.asset_library_javascript_tags(:m)
20
+ end
21
+
22
+ it('should output nothing when a module is empty') do
23
+ m = mock(:assets => [])
24
+ AssetLibrary.stub!(:asset_module).and_return(m)
25
+ h.asset_library_javascript_tags(:m).should == ''
26
+ end
27
+
28
+ it('should output a <script> tag for a file') do
29
+ m = mock(:assets => [a('/f.js')])
30
+ AssetLibrary.stub!(:asset_module).and_return(m)
31
+ h.asset_library_javascript_tags(:m).should == '<script type="text/javascript" src="/f.js?123"></script>'
32
+ end
33
+
34
+ it('should join <script> tags with newlines') do
35
+ m = mock(:assets => [a('/f.js'), a('/f2.js')])
36
+ AssetLibrary.stub!(:asset_module).and_return(m)
37
+ 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>'
38
+ end
39
+ end
40
+
41
+ describe('when caching') do
42
+ before(:each) do
43
+ AssetLibrary.stub!(:cache).and_return(true)
44
+ end
45
+
46
+ it('should output a single <script> tag with the cache filename') do
47
+ m = mock(:cache_asset => a('/cache.js'))
48
+ AssetLibrary.stub!(:asset_module).and_return(m)
49
+ h.asset_library_javascript_tags(:m).should == '<script type="text/javascript" src="/cache.js?123"></script>'
50
+ end
51
+ end
52
+ end
53
+
54
+ describe('#asset_library_stylesheet_tags') do
55
+ describe('when not caching') do
56
+ before(:each) do
57
+ AssetLibrary.stub!(:cache).and_return(false)
58
+ end
59
+
60
+ it('should fetch using asset_module') do
61
+ m = mock(:assets => [])
62
+ AssetLibrary.should_receive(:asset_module).with(:m).and_return(m)
63
+ h.asset_library_stylesheet_tags(:m)
64
+ end
65
+
66
+ it('should output nothing when a module is empty') do
67
+ m = mock(:assets => [])
68
+ AssetLibrary.stub!(:asset_module).and_return(m)
69
+ h.asset_library_stylesheet_tags(:m).should == ''
70
+ end
71
+
72
+ it('should output a single <script> with a single @import when there is one file') do
73
+ m = mock(:assets => [a('/f.css')])
74
+ AssetLibrary.stub!(:asset_module).and_return(m)
75
+ h.asset_library_stylesheet_tags(:m).should == "<style type=\"text/css\">\n@import \"\/f.css?123\";\n</style>"
76
+ end
77
+
78
+ it('should append extra_suffix to the cache filename') do
79
+ m = mock
80
+ m.should_receive(:assets).with('e').and_return([a('f.e.css')])
81
+ AssetLibrary.stub!(:asset_module).and_return(m)
82
+ h.asset_library_stylesheet_tags(:m, 'e').should == "<style type=\"text/css\">\n@import \"f.e.css?123\";\n</style>"
83
+ end
84
+
85
+ it('should output a single <script> tag with 30 @import') do
86
+ m = mock(:assets => (1..30).collect{|i| a("/f#{i}.css") })
87
+ AssetLibrary.stub!(:asset_module).and_return(m)
88
+ h.asset_library_stylesheet_tags(:m).should =~ /\<style type=\"text\/css\"\>(\n@import \"\/f\d+.css\?123\";){30}\n\<\/style\>/
89
+ end
90
+
91
+ it('should output two <script> tags with 31 @imports') do
92
+ m = mock(:assets => (1..31).collect{|i| a("/f#{i}.css") })
93
+ AssetLibrary.stub!(:asset_module).and_return(m)
94
+ 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\>/
95
+ end
96
+ end
97
+
98
+ describe('when caching') do
99
+ before(:each) do
100
+ AssetLibrary.stub!(:cache).and_return(true)
101
+ end
102
+
103
+ it('should output a single <style> tag with the cache filename') do
104
+ m = mock(:cache_asset => a('/cache.css'))
105
+ AssetLibrary.stub!(:asset_module).and_return(m)
106
+ h.asset_library_stylesheet_tags(:m).should == '<link rel="stylesheet" type="text/css" href="/cache.css?123" />'
107
+ end
108
+
109
+ it('should append extra_suffix to the cache filename') do
110
+ m = mock
111
+ m.should_receive(:cache_asset).with('e').and_return(a('/cache.e.css'))
112
+ AssetLibrary.stub!(:asset_module).and_return(m)
113
+ h.asset_library_stylesheet_tags(:m, 'e').should == '<link rel="stylesheet" type="text/css" href="/cache.e.css?123" />'
114
+ end
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def a(path)
121
+ File.stub!(:mtime).and_return(Time.at(123))
122
+ AssetLibrary::Asset.new(path)
123
+ end
124
+
125
+ def h
126
+ c = Class.new do
127
+ include AssetLibrary::Helpers
128
+ end
129
+ o = c.new
130
+ end
131
+ 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,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adamh-asset_library
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - adamh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-07-15 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: adam@adamhooper.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ files:
25
+ - lib/asset_library.rb
26
+ - lib/asset_library/asset.rb
27
+ - lib/asset_library/asset_module.rb
28
+ - lib/asset_library/helpers.rb
29
+ - lib/asset_library/rake_tasks.rb
30
+ - lib/asset_library/util.rb
31
+ - rails/init.rb
32
+ - README.rdoc
33
+ has_rdoc: true
34
+ homepage: http://github.com/adamh/asset_library
35
+ post_install_message:
36
+ rdoc_options:
37
+ - --charset=UTF-8
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ version:
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ requirements: []
53
+
54
+ rubyforge_project:
55
+ rubygems_version: 1.2.0
56
+ signing_key:
57
+ specification_version: 2
58
+ summary: Manage and bundle CSS and JavaScript files
59
+ test_files:
60
+ - spec/asset_library_spec.rb
61
+ - spec/asset_library/asset_spec.rb
62
+ - spec/asset_library/helpers_spec.rb
63
+ - spec/asset_library/asset_module_spec.rb
64
+ - spec/spec_helper.rb