jammit-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. data/LICENSE +22 -0
  2. data/README +24 -0
  3. data/README.rdoc +17 -0
  4. data/Rakefile +59 -0
  5. data/VERSION +1 -0
  6. data/bin/jammit +13 -0
  7. data/lib/assets.example.yml +14 -0
  8. data/lib/jammit-core.rb +219 -0
  9. data/lib/jammit-core/cli.rb +87 -0
  10. data/lib/jammit-core/command_line.rb +79 -0
  11. data/lib/jammit-core/compressor.rb +214 -0
  12. data/lib/jammit-core/config.rb +82 -0
  13. data/lib/jammit-core/dependencies.rb +23 -0
  14. data/lib/jammit-core/helper.rb +77 -0
  15. data/lib/jammit-core/jst.js +1 -0
  16. data/lib/jammit-core/packager.rb +163 -0
  17. data/lib/jammit-core/routes.rb +16 -0
  18. data/lib/jammit-core/ui.rb +55 -0
  19. data/test/config/assets-broken.yml +16 -0
  20. data/test/config/assets-closure.yml +16 -0
  21. data/test/config/assets-compression-disabled.yml +16 -0
  22. data/test/config/assets-css.yml +6 -0
  23. data/test/config/assets-erb.yml +13 -0
  24. data/test/config/assets-no-java.yml +16 -0
  25. data/test/config/assets.yml +13 -0
  26. data/test/fixtures/jammed/test-closure.js +1 -0
  27. data/test/fixtures/jammed/test-datauri.css +1 -0
  28. data/test/fixtures/jammed/test-line-break.css +8 -0
  29. data/test/fixtures/jammed/test-mhtml.css +17 -0
  30. data/test/fixtures/jammed/test-uncompressed.css +36 -0
  31. data/test/fixtures/jammed/test-uncompressed.js +13 -0
  32. data/test/fixtures/jammed/test.css +1 -0
  33. data/test/fixtures/jammed/test.js +1 -0
  34. data/test/fixtures/jammed/test.jst +6 -0
  35. data/test/fixtures/jammed/test2.jst +6 -0
  36. data/test/fixtures/src/test1.css +11 -0
  37. data/test/fixtures/src/test1.js +8 -0
  38. data/test/fixtures/src/test1.jst +1 -0
  39. data/test/fixtures/src/test2.css +20 -0
  40. data/test/fixtures/src/test2.js +5 -0
  41. data/test/fixtures/src/test2.jst +5 -0
  42. data/test/fixtures/src/test_fonts.css +5 -0
  43. data/test/fixtures/tags/css_includes.html +6 -0
  44. data/test/fixtures/tags/css_individual_includes.html +3 -0
  45. data/test/fixtures/tags/css_plain_includes.html +1 -0
  46. data/test/fixtures/tags/css_print.html +6 -0
  47. data/test/public/embed/DroidSansMono.eot +0 -0
  48. data/test/public/embed/DroidSansMono.ttf +0 -0
  49. data/test/public/embed/asterisk_orange.png +0 -0
  50. data/test/public/embed/asterisk_yellow.png +0 -0
  51. data/test/test_helper.rb +44 -0
  52. data/test/unit/command_line_test.rb +28 -0
  53. data/test/unit/test_closure_compressor.rb +24 -0
  54. data/test/unit/test_compressor.rb +24 -0
  55. data/test/unit/test_configuration.rb +71 -0
  56. data/test/unit/test_in_the_wrong_directory.rb +40 -0
  57. data/test/unit/test_jammit_helpers.rb +44 -0
  58. data/test/unit/test_packager.rb +77 -0
  59. metadata +187 -0
@@ -0,0 +1,79 @@
1
+ require 'optparse'
2
+ require File.expand_path(File.dirname(__FILE__) + '/../jammit')
3
+
4
+ module Jammit
5
+
6
+ # The @CommandLine@ is able to compress, pre-package, and pre-gzip all the
7
+ # assets specified in the configuration file, in order to avoid an initial
8
+ # round of slow requests after a fresh deployment.
9
+ class CommandLine
10
+
11
+ BANNER = <<-EOS
12
+
13
+ Usage: jammit OPTIONS
14
+
15
+ Run jammit inside a Rails application to compresses all JS, CSS,
16
+ and JST according to config/assets.yml, saving the packaged
17
+ files and corresponding gzipped versions.
18
+
19
+ If you're using "embed_assets", and you wish to precompile the
20
+ MHTML stylesheet variants, you must specify the "base-url".
21
+
22
+ Options:
23
+ EOS
24
+
25
+ # The @Jammit::CommandLine@ runs from the contents of @ARGV@.
26
+ def initialize
27
+ parse_options
28
+ ensure_configuration_file
29
+ Jammit.load_configuration(@options[:config_path])
30
+ Jammit.packager.force = @options[:force]
31
+ Jammit.packager.precache_all(@options[:output_folder], @options[:base_url])
32
+ end
33
+
34
+
35
+ private
36
+
37
+ # Make sure that we have a readable configuration file. The @jammit@
38
+ # command can't run without one.
39
+ def ensure_configuration_file
40
+ config = @options[:config_path]
41
+ return true if File.exists?(config) && File.readable?(config)
42
+ puts "Could not find the asset configuration file \"#{config}\""
43
+ exit(1)
44
+ end
45
+
46
+ # Uses @OptionParser@ to grab the options: *--output*, *--config*, and
47
+ # *--base-url*...
48
+ def parse_options
49
+ @options = {
50
+ :config_path => Jammit::DEFAULT_CONFIG_PATH,
51
+ :output_folder => nil,
52
+ :base_url => nil,
53
+ :force => false
54
+ }
55
+ @option_parser = OptionParser.new do |opts|
56
+ opts.on('-o', '--output PATH', 'output folder for packages (default: "public/assets")') do |output_folder|
57
+ @options[:output_folder] = output_folder
58
+ end
59
+ opts.on('-c', '--config PATH', 'path to assets.yml (default: "config/assets.yml")') do |config_path|
60
+ @options[:config_path] = config_path
61
+ end
62
+ opts.on('-u', '--base-url URL', 'base URL for MHTML (ex: "http://example.com")') do |base_url|
63
+ @options[:base_url] = base_url
64
+ end
65
+ opts.on('-f', '--force', 'force a rebuild of all assets') do |force|
66
+ @options[:force] = force
67
+ end
68
+ opts.on_tail('-v', '--version', 'display Jammit version') do
69
+ puts "Jammit version #{Jammit::VERSION}"
70
+ exit
71
+ end
72
+ end
73
+ @option_parser.banner = BANNER
74
+ @option_parser.parse!(ARGV)
75
+ end
76
+
77
+ end
78
+
79
+ end
@@ -0,0 +1,214 @@
1
+ module Jammit
2
+
3
+ # Uses the YUI Compressor or Closure Compiler to compress JavaScript.
4
+ # Always uses YUI to compress CSS (Which means that Java must be installed.)
5
+ # Also knows how to create a concatenated JST file.
6
+ # If "embed_assets" is turned on, creates "mhtml" and "datauri" versions of
7
+ # all stylesheets, with all enabled assets inlined into the css.
8
+ class Compressor
9
+
10
+ # Mapping from extension to mime-type of all embeddable assets.
11
+ EMBED_MIME_TYPES = {
12
+ '.png' => 'image/png',
13
+ '.jpg' => 'image/jpeg',
14
+ '.jpeg' => 'image/jpeg',
15
+ '.gif' => 'image/gif',
16
+ '.tif' => 'image/tiff',
17
+ '.tiff' => 'image/tiff',
18
+ '.ttf' => 'font/truetype',
19
+ '.otf' => 'font/opentype'
20
+ }
21
+
22
+ # Font extensions for which we allow embedding:
23
+ EMBED_EXTS = EMBED_MIME_TYPES.keys
24
+ EMBED_FONTS = ['.ttf', '.otf']
25
+
26
+ # Maximum size for embeddable images (an IE8 limitation).
27
+ MAX_IMAGE_SIZE = 32 * 1024
28
+
29
+ # CSS asset-embedding regexes for URL rewriting.
30
+ EMBED_DETECTOR = /url\(['"]?([^\s)]+\.[a-z]+)(\?\d+)?['"]?\)/
31
+ EMBEDDABLE = /[\A\/]embed\//
32
+ EMBED_REPLACER = /url\(__EMBED__([^\s)]+)(\?\d+)?\)/
33
+
34
+ # MHTML file constants.
35
+ MHTML_START = "/*\r\nContent-Type: multipart/related; boundary=\"JAMMIT_MHTML_SEPARATOR\"\r\n\r\n"
36
+ MHTML_SEPARATOR = "--JAMMIT_MHTML_SEPARATOR\r\n"
37
+ MHTML_END = "*/\r\n"
38
+
39
+ # JST file constants.
40
+ JST_START = "(function(){"
41
+ JST_END = "})();"
42
+
43
+ COMPRESSORS = {
44
+ :yui => YUI::JavaScriptCompressor,
45
+ :closure => Closure::Compiler
46
+ }
47
+
48
+ DEFAULT_OPTIONS = {
49
+ :yui => {:munge => true},
50
+ :closure => {}
51
+ }
52
+
53
+ # Creating a compressor initializes the internal YUI Compressor from
54
+ # the "yui-compressor" gem, or the internal Closure Compiler from the
55
+ # "closure-compiler" gem.
56
+ def initialize
57
+ @css_compressor = YUI::CssCompressor.new(Jammit.config[:css_compressor_options] || {})
58
+ flavor = Jammit.config[:javascript_compressor] || Jammit::DEFAULT_COMPRESSOR
59
+ @options = DEFAULT_OPTIONS[flavor].merge(Jammit.config[:compressor_options] || {})
60
+ @js_compressor = COMPRESSORS[flavor].new(@options)
61
+ end
62
+
63
+ # Concatenate together a list of JavaScript paths, and pass them through the
64
+ # YUI Compressor (with munging enabled).
65
+ def compress_js(paths)
66
+ js = concatenate(paths)
67
+ Jammit.config[:compress_assets] ? @js_compressor.compress(js) : js
68
+ end
69
+
70
+ # Concatenate and compress a list of CSS stylesheets. When compressing a
71
+ # :datauri or :mhtml variant, post-processes the result to embed
72
+ # referenced assets.
73
+ def compress_css(paths, variant=nil, asset_url=nil)
74
+ css = concatenate_and_tag_assets(paths, variant)
75
+ css = @css_compressor.compress(css) if Jammit.compress_assets
76
+ case variant
77
+ when nil then return css
78
+ when :datauri then return with_data_uris(css)
79
+ when :mhtml then return with_mhtml(css, asset_url)
80
+ else raise PackageNotFound, "\"#{variant}\" is not a valid stylesheet variant"
81
+ end
82
+ end
83
+
84
+ # Compiles a single JST file by writing out a javascript that adds
85
+ # template properties to a top-level template namespace object. Adds a
86
+ # JST-compilation function to the top of the package, unless you've
87
+ # specified your own preferred function, or turned it off.
88
+ # JST templates are named with the basename of their file.
89
+ def compile_jst(paths)
90
+ namespace = Jammit.template_namespace
91
+ compiled = paths.map do |path|
92
+ template_name = File.basename(path, File.extname(path))
93
+ contents = File.read(path).gsub(/\n/, '').gsub("'", '\\\\\'')
94
+ "#{namespace}.#{template_name} = #{Jammit.template_function}('#{contents}');"
95
+ end
96
+ compiler = Jammit.config[:include_jst_script] ? File.read(Jammit::DEFAULT_JST_SCRIPT) : '';
97
+ setup_namespace = "#{namespace} = #{namespace} || {};"
98
+ [JST_START, setup_namespace, compiler, compiled, JST_END].flatten.join("\n")
99
+ end
100
+
101
+
102
+ private
103
+
104
+ # In order to support embedded assets from relative paths, we need to
105
+ # expand the paths before contatenating the CSS together and losing the
106
+ # location of the original stylesheet path. Validate the assets while we're
107
+ # at it.
108
+ def concatenate_and_tag_assets(paths, variant=nil)
109
+ stylesheets = [paths].flatten.map do |css_path|
110
+ File.read(css_path).gsub(EMBED_DETECTOR) do |url|
111
+ ipath, cpath = Pathname.new($1), Pathname.new(File.expand_path(css_path))
112
+ is_url = URI.parse($1).absolute?
113
+ is_url ? url : "url(#{construct_asset_path(ipath, cpath, variant)})"
114
+ end
115
+ end
116
+ stylesheets.join("\n")
117
+ end
118
+
119
+ # Re-write all enabled asset URLs in a stylesheet with their corresponding
120
+ # Data-URI Base-64 encoded asset contents.
121
+ def with_data_uris(css)
122
+ css.gsub(EMBED_REPLACER) do |url|
123
+ "url(\"data:#{mime_type($1)};charset=utf-8;base64,#{encoded_contents($1)}\")"
124
+ end
125
+ end
126
+
127
+ # Re-write all enabled asset URLs in a stylesheet with the MHTML equivalent.
128
+ # The newlines ("\r\n") in the following method are critical. Without them
129
+ # your MHTML will look identical, but won't work.
130
+ def with_mhtml(css, asset_url)
131
+ paths, index = {}, 0
132
+ css = css.gsub(EMBED_REPLACER) do |url|
133
+ i = paths[$1] ||= "#{index += 1}-#{File.basename($1)}"
134
+ "url(mhtml:#{asset_url}!#{i})"
135
+ end
136
+ mhtml = paths.sort.map do |path, identifier|
137
+ mime, contents = mime_type(path), encoded_contents(path)
138
+ [MHTML_SEPARATOR, "Content-Location: #{identifier}\r\n", "Content-Type: #{mime}\r\n", "Content-Transfer-Encoding: base64\r\n\r\n", contents, "\r\n"]
139
+ end
140
+ [MHTML_START, mhtml, MHTML_END, css].flatten.join('')
141
+ end
142
+
143
+ # Return a rewritten asset URL for a new stylesheet -- the asset should
144
+ # be tagged for embedding if embeddable, and referenced at the correct level
145
+ # if relative.
146
+ def construct_asset_path(asset_path, css_path, variant)
147
+ public_path = absolute_path(asset_path, css_path)
148
+ return "__EMBED__#{public_path}" if embeddable?(public_path, variant)
149
+ source = asset_path.absolute? ? asset_path.to_s : relative_path(public_path)
150
+ rewrite_asset_path(source, public_path)
151
+ end
152
+
153
+ # Get the site-absolute public path for an asset file path that may or may
154
+ # not be relative, given the path of the stylesheet that contains it.
155
+ def absolute_path(asset_pathname, css_pathname)
156
+ (asset_pathname.absolute? ?
157
+ Pathname.new(File.join(PUBLIC_ROOT, asset_pathname)) :
158
+ css_pathname.dirname + asset_pathname).cleanpath
159
+ end
160
+
161
+ # CSS assets that are referenced by relative paths, and are *not* being
162
+ # embedded, must be rewritten relative to the newly-merged stylesheet path.
163
+ def relative_path(absolute_path)
164
+ File.join('../', absolute_path.sub(PUBLIC_ROOT, ''))
165
+ end
166
+
167
+ # Similar to the AssetTagHelper's method of the same name, this will
168
+ # append the RAILS_ASSET_ID cache-buster to URLs, if it's defined.
169
+ def rewrite_asset_path(path, file_path)
170
+ asset_id = rails_asset_id(file_path)
171
+ asset_id.blank? ? path : "#{path}?#{asset_id}"
172
+ end
173
+
174
+ # Similar to the AssetTagHelper's method of the same name, this will
175
+ # determine the correct asset id for a file.
176
+ def rails_asset_id(path)
177
+ asset_id = ENV["RAILS_ASSET_ID"]
178
+ return asset_id if asset_id
179
+ File.exists?(path) ? File.mtime(path).to_i.to_s : ''
180
+ end
181
+
182
+ # An asset is valid for embedding if it exists, is less than 32K, and is
183
+ # stored somewhere inside of a folder named "embed".
184
+ # IE does not support Data-URIs larger than 32K, and you probably shouldn't
185
+ # be embedding assets that large in any case.
186
+ def embeddable?(asset_path, variant)
187
+ font = EMBED_FONTS.include?(asset_path.extname)
188
+ return false unless variant
189
+ return false unless asset_path.to_s.match(EMBEDDABLE) && asset_path.exist?
190
+ return false unless EMBED_EXTS.include?(asset_path.extname)
191
+ return false unless font || asset_path.size < MAX_IMAGE_SIZE
192
+ return false if font && variant == :mhtml
193
+ true
194
+ end
195
+
196
+ # Return the Base64-encoded contents of an asset on a single line.
197
+ def encoded_contents(asset_path)
198
+ data = File.open(asset_path, 'rb'){|f| f.read }
199
+ Base64.encode64(data).gsub(/\n/, '')
200
+ end
201
+
202
+ # Grab the mime-type of an asset, by filename.
203
+ def mime_type(asset_path)
204
+ EMBED_MIME_TYPES[File.extname(asset_path)]
205
+ end
206
+
207
+ # Concatenate together a list of asset files.
208
+ def concatenate(paths)
209
+ [paths].flatten.map {|p| File.read(p) }.join("\n")
210
+ end
211
+
212
+ end
213
+
214
+ end
@@ -0,0 +1,82 @@
1
+ module Jammit
2
+ module Config
3
+
4
+ CONFIG_TEMPLATE = Jammit::ROOT + '/assets.example.yml'
5
+
6
+ class << self
7
+
8
+ def dispatch(command, *params)
9
+ case command
10
+ when :create
11
+ create
12
+ when :set
13
+ set(params.shift, params.shift)
14
+ end
15
+ end
16
+
17
+ ##
18
+ # load configuration file
19
+ # @param {String} path
20
+ # @param {Boolean} force reloading of config file
21
+ #
22
+ def load(force=false)
23
+ raise Jammit::ConfigurationNotFound, "could not find the \"#{Jammit.config_path}\" configuration file" unless File.exists?(Jammit.config_path) && File.readable?(Jammit.config_path)
24
+ if !@config || force == true
25
+ @config = YAML.load(ERB.new(File.read(Jammit.config_path)).result)
26
+ end
27
+ @config
28
+ end
29
+
30
+ private
31
+
32
+ ##
33
+ # Write config to file
34
+ #
35
+ def write(config)
36
+ File.open(Jammit.config_path, "w") {|f|
37
+ f << config.to_yaml
38
+ }
39
+ end
40
+
41
+ ##
42
+ # If a config path is given, use that as the root, otherwise use DEFAULT_CONFIG_PATH
43
+ #
44
+ def create
45
+ if File.exists?(Jammit.config_path)
46
+ raise Jammit::AlreadyConfigured.new(" Config file #{Jammit.config_path} already exists.")
47
+ end
48
+ FileUtils.mkdir_p(File.dirname(Jammit.config_path))
49
+ FileUtils.cp(CONFIG_TEMPLATE, Jammit.config_path)
50
+
51
+ parts = Jammit.config_path.split('/')
52
+ name = parts.pop
53
+ dir = parts.pop
54
+ Jammit.ui.confirm(" create #{dir}/#{name}")
55
+ end
56
+
57
+ ##
58
+ # Set a config property in file
59
+ # @param {String} name
60
+ # @param {String} value
61
+ #
62
+ def set(name, value)
63
+ value = (value == "true") ? true : (value == "false") ? false : value
64
+
65
+ config = load(true)
66
+ option = config[name]
67
+
68
+ # careful not to clobber raexisting values if updating a hash
69
+ if value.kind_of?(Hash) && option.kind_of?(Hash)
70
+ value.keys.each do |k|
71
+ option[k] = value[k]
72
+ end
73
+ config.update(name => option)
74
+ else
75
+ config.update(name => value)
76
+ end
77
+ write(config)
78
+ Jammit.ui.confirm(" set #{name}")
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,23 @@
1
+ # Standard Library Dependencies:
2
+ require 'uri'
3
+ require 'erb'
4
+ require 'zlib'
5
+ require 'base64'
6
+ require 'pathname'
7
+ require 'fileutils'
8
+ require 'json'
9
+
10
+ # Pull in some extlib goodies
11
+ require 'extlib/class'
12
+ require 'extlib/hash'
13
+ require 'extlib/mash'
14
+ require 'extlib/inflection'
15
+
16
+ # Gem Dependencies:
17
+ require 'rubygems'
18
+ require 'yui/compressor'
19
+ require 'closure-compiler'
20
+
21
+ # Jammit Core:
22
+ require 'jammit-core/compressor'
23
+ require 'jammit-core/packager'
@@ -0,0 +1,77 @@
1
+ module Jammit
2
+
3
+ # The Jammit::Helper module, which is made available to every view, provides
4
+ # helpers for writing out HTML tags for asset packages. In development you
5
+ # get the ordered list of source files -- in any other environment, a link
6
+ # to the cached packages.
7
+ module Helper
8
+
9
+ DATA_URI_START = "<!--[if (!IE)|(gte IE 8)]><!-->"
10
+ DATA_URI_END = "<!--<![endif]-->"
11
+ MHTML_START = "<!--[if lte IE 7]>"
12
+ MHTML_END = "<![endif]-->"
13
+
14
+ # If embed_assets is turned on, writes out links to the Data-URI and MHTML
15
+ # versions of the stylesheet package, otherwise the package is regular
16
+ # compressed CSS, and in development the stylesheet URLs are passed verbatim.
17
+ def include_stylesheets(*packages)
18
+ options = packages.extract_options!
19
+ return individual_stylesheets(packages, options) unless Jammit.package_assets
20
+ disabled = (options.delete(:embed_assets) == false) || (options.delete(:embed_images) == false)
21
+ return packaged_stylesheets(packages, options) if disabled || !Jammit.embed_assets
22
+ return embedded_image_stylesheets(packages, options)
23
+ end
24
+
25
+ # Writes out the URL to the bundled and compressed javascript package,
26
+ # except in development, where it references the individual scripts.
27
+ def include_javascripts(*packages)
28
+ tags = packages.map do |pack|
29
+ Jammit.package_assets ? Jammit.asset_url(pack, :js) : Jammit.packager.individual_urls(pack.to_sym, :js)
30
+ end
31
+ javascript_include_tag(tags.flatten)
32
+ end
33
+
34
+ # Writes out the URL to the concatenated and compiled JST file -- we always
35
+ # have to pre-process it, even in development.
36
+ def include_templates(*packages)
37
+ javascript_include_tag(packages.map {|pack| Jammit.asset_url(pack, :jst) })
38
+ end
39
+
40
+
41
+ private
42
+
43
+ # HTML tags, in order, for all of the individual stylesheets.
44
+ def individual_stylesheets(packages, options)
45
+ tags_with_options(packages, options) {|p| Jammit.packager.individual_urls(p.to_sym, :css) }
46
+ end
47
+
48
+ # HTML tags for the stylesheet packages.
49
+ def packaged_stylesheets(packages, options)
50
+ tags_with_options(packages, options) {|p| Jammit.asset_url(p, :css) }
51
+ end
52
+
53
+ # HTML tags for the 'datauri', and 'mhtml' versions of the packaged
54
+ # stylesheets, using conditional comments to load the correct variant.
55
+ def embedded_image_stylesheets(packages, options)
56
+ datauri_tags = tags_with_options(packages, options) {|p| Jammit.asset_url(p, :css, :datauri) }
57
+ ie_tags = Jammit.mhtml_enabled ?
58
+ tags_with_options(packages, options) {|p| Jammit.asset_url(p, :css, :mhtml) } :
59
+ packaged_stylesheets(packages, options)
60
+ [DATA_URI_START, datauri_tags, DATA_URI_END, MHTML_START, ie_tags, MHTML_END].join("\n")
61
+ end
62
+
63
+ # Generate the stylesheet tags for a batch of packages, with options, by
64
+ # yielding each package to a block.
65
+ def tags_with_options(packages, options)
66
+ packages = packages.dup
67
+ packages.map! {|package| yield package }.flatten!
68
+ packages.push(options) unless options.empty?
69
+ stylesheet_link_tag(*packages)
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+
76
+ # Include the Jammit asset helpers in all views, a-la ApplicationHelper.
77
+ ::ActionView::Base.send(:include, Jammit::Helper)