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 +163 -0
- data/lib/asset_library.rb +58 -0
- data/lib/asset_library/asset.rb +33 -0
- data/lib/asset_library/asset_module.rb +105 -0
- data/lib/asset_library/helpers.rb +40 -0
- data/lib/asset_library/rake_tasks.rb +25 -0
- data/lib/asset_library/util.rb +11 -0
- data/rails/init.rb +3 -0
- data/spec/asset_library/asset_module_spec.rb +193 -0
- data/spec/asset_library/asset_spec.rb +49 -0
- data/spec/asset_library/helpers_spec.rb +131 -0
- data/spec/asset_library_spec.rb +97 -0
- data/spec/spec_helper.rb +4 -0
- metadata +64 -0
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,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
|
data/spec/spec_helper.rb
ADDED
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
|