adamh-asset_library 0.1.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 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