jammit 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.
- data/LICENSE +22 -0
- data/README +21 -0
- data/Rakefile +38 -0
- data/bin/jammit +5 -0
- data/jammit.gemspec +47 -0
- data/lib/jammit.rb +117 -0
- data/lib/jammit/command_line.rb +74 -0
- data/lib/jammit/compressor.rb +125 -0
- data/lib/jammit/controller.rb +91 -0
- data/lib/jammit/helper.rb +66 -0
- data/lib/jammit/jst.js +1 -0
- data/lib/jammit/packager.rb +113 -0
- data/lib/jammit/routes.rb +16 -0
- metadata +92 -0
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2009 Jeremy Ashkenas, DocumentCloud
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
==
|
2
|
+
_ _ __ __ __ __ ___ _____
|
3
|
+
_ | |/_\ | \/ | \/ |_ _|_ _|
|
4
|
+
| || / _ \| |\/| | |\/| || | | |
|
5
|
+
\__/_/ \_\_| |_|_| |_|___| |_|
|
6
|
+
|
7
|
+
|
8
|
+
Jammit is an industrial strength asset packaging library for Rails,
|
9
|
+
providing both the CSS and JavaScript concatenation and compression
|
10
|
+
that you'd expect, as well as ahead-of-time gzipping, built-in JavaScript
|
11
|
+
template support, and optional Data-URI / MHTML image embedding.
|
12
|
+
|
13
|
+
For documentation, usage, and examples, see:
|
14
|
+
http://documentcloud.github.com/jammit/
|
15
|
+
|
16
|
+
To suggest a feature or report a bug:
|
17
|
+
http://github.com/documentcloud/jammit/issues/
|
18
|
+
|
19
|
+
For internal source docs, see:
|
20
|
+
http://documentcloud.github.com/jammit/doc/
|
21
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
desc 'Run all tests'
|
4
|
+
task :test do
|
5
|
+
$LOAD_PATH.unshift(File.expand_path('test'))
|
6
|
+
require 'redgreen' if Gem.available?('redgreen')
|
7
|
+
require 'test/unit'
|
8
|
+
Dir['test/**/test_*.rb'].each {|test| require test }
|
9
|
+
end
|
10
|
+
|
11
|
+
desc 'Generate YARD Documentation'
|
12
|
+
task :doc do
|
13
|
+
sh "mv README TEMPME"
|
14
|
+
sh "yardoc"
|
15
|
+
sh "mv TEMPME README"
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Deploy the Github Page"
|
19
|
+
task :deploy do
|
20
|
+
sh "git co gh-pages && git merge master && git push github gh-pages && git co master"
|
21
|
+
end
|
22
|
+
|
23
|
+
namespace :gem do
|
24
|
+
|
25
|
+
desc 'Build and install the jammit gem'
|
26
|
+
task :install do
|
27
|
+
sh "gem build jammit.gemspec"
|
28
|
+
sh "sudo gem install #{Dir['*.gem'].join(' ')} --local --no-ri --no-rdoc"
|
29
|
+
end
|
30
|
+
|
31
|
+
desc 'Uninstall the jammit gem'
|
32
|
+
task :uninstall do
|
33
|
+
sh "sudo gem uninstall -x jammit"
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
task :default => :test
|
data/bin/jammit
ADDED
data/jammit.gemspec
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'jammit'
|
3
|
+
s.version = '0.1.0' # Keep version in sync with jammit.rb
|
4
|
+
s.date = '2009-11-06'
|
5
|
+
|
6
|
+
s.homepage = "http://documentcloud.github.com/jammit/"
|
7
|
+
s.summary = "Industrial Strength Asset Packaging for Rails"
|
8
|
+
s.description = <<-EOS
|
9
|
+
Jammit is an industrial strength asset packaging library for Rails,
|
10
|
+
providing both the CSS and JavaScript concatenation and compression
|
11
|
+
that you'd expect, as well as ahead-of-time gzipping, built-in JavaScript
|
12
|
+
template support, and optional Data-URI / MHTML image embedding.
|
13
|
+
EOS
|
14
|
+
|
15
|
+
s.authors = ['Jeremy Ashkenas']
|
16
|
+
s.email = 'jeremy@documentcloud.org'
|
17
|
+
s.rubyforge_project = 'jammit'
|
18
|
+
|
19
|
+
s.require_paths = ['lib']
|
20
|
+
s.executables = ['jammit']
|
21
|
+
|
22
|
+
s.has_rdoc = true
|
23
|
+
s.extra_rdoc_files = ['README']
|
24
|
+
s.rdoc_options << '--title' << 'Jammit' <<
|
25
|
+
'--exclude' << 'test' <<
|
26
|
+
'--main' << 'README' <<
|
27
|
+
'--all'
|
28
|
+
|
29
|
+
s.add_dependency 'rails', ['>= 2.0.0']
|
30
|
+
s.add_dependency 'yui-compressor', ['>= 0.9.1']
|
31
|
+
|
32
|
+
s.files = %w(
|
33
|
+
bin/jammit
|
34
|
+
jammit.gemspec
|
35
|
+
lib/jammit.rb
|
36
|
+
lib/jammit/command_line.rb
|
37
|
+
lib/jammit/compressor.rb
|
38
|
+
lib/jammit/controller.rb
|
39
|
+
lib/jammit/helper.rb
|
40
|
+
lib/jammit/jst.js
|
41
|
+
lib/jammit/packager.rb
|
42
|
+
lib/jammit/routes.rb
|
43
|
+
LICENSE
|
44
|
+
Rakefile
|
45
|
+
README
|
46
|
+
)
|
47
|
+
end
|
data/lib/jammit.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
$LOAD_PATH.push File.expand_path(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
# @Jammit@ is the central namespace for all Jammit classes, and provides access
|
4
|
+
# to all of the configuration options.
|
5
|
+
module Jammit
|
6
|
+
|
7
|
+
VERSION = "0.1.0"
|
8
|
+
|
9
|
+
ROOT = File.expand_path(File.dirname(__FILE__) + '/..')
|
10
|
+
|
11
|
+
DEFAULT_CONFIG_PATH = "config/assets.yml"
|
12
|
+
|
13
|
+
DEFAULT_PACKAGE_PATH = "assets"
|
14
|
+
|
15
|
+
DEFAULT_JST_SCRIPT = "#{ROOT}/lib/jammit/jst.js"
|
16
|
+
|
17
|
+
DEFAULT_JST_COMPILER = "template"
|
18
|
+
|
19
|
+
# Jammit raises a @PackageNotFound@ exception when a non-existent package is
|
20
|
+
# requested by a browser -- rendering a 404.
|
21
|
+
class PackageNotFound < NameError; end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
attr_reader :configuration, :template_function, :embed_images, :package_path,
|
25
|
+
:package_assets, :mhtml_enabled, :include_jst_script
|
26
|
+
end
|
27
|
+
|
28
|
+
# The minimal required configuration.
|
29
|
+
@configuration = {}
|
30
|
+
@package_path = DEFAULT_PACKAGE_PATH
|
31
|
+
|
32
|
+
# Load the complete asset configuration from the specified @config_path@.
|
33
|
+
def self.load_configuration(config_path)
|
34
|
+
return unless config_path && File.exists?(config_path)
|
35
|
+
@config_path = config_path
|
36
|
+
@configuration = conf = YAML.load_file(@config_path).symbolize_keys
|
37
|
+
@package_path = conf[:package_path] || DEFAULT_PACKAGE_PATH
|
38
|
+
@embed_images = conf[:embed_images]
|
39
|
+
@mhtml_enabled = @embed_images && @embed_images != "datauri"
|
40
|
+
set_package_assets(conf[:package_assets])
|
41
|
+
set_template_function(conf[:template_function])
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
# Force a reload by resetting the Packager and reloading the configuration.
|
46
|
+
# In development, this will be called before every request to the
|
47
|
+
# @Jammit::Controller@.
|
48
|
+
def self.reload!
|
49
|
+
Thread.current[:jammit_packager] = nil
|
50
|
+
load_configuration(@config_path)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Keep a global (thread-local) reference to a @Jammit::Packager@, to avoid
|
54
|
+
# recomputing asset lists unnecessarily.
|
55
|
+
def self.packager
|
56
|
+
Thread.current[:jammit_packager] ||= Packager.new
|
57
|
+
end
|
58
|
+
|
59
|
+
# Generate the base filename for a version of a given package.
|
60
|
+
def self.filename(package, extension, suffix=nil)
|
61
|
+
suffix_part = suffix ? "-#{suffix}" : ''
|
62
|
+
"#{package}#{suffix_part}.#{extension}"
|
63
|
+
end
|
64
|
+
|
65
|
+
# Generates the server-absolute URL to an asset package.
|
66
|
+
def self.asset_url(package, extension, suffix=nil, mtime=nil)
|
67
|
+
timestamp = mtime ? "?#{mtime.to_i}" : ''
|
68
|
+
"/#{package_path}/#{filename(package, extension, suffix)}#{timestamp}"
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def self.set_package_assets(value)
|
75
|
+
package_env = !defined?(RAILS_ENV) || RAILS_ENV != 'development'
|
76
|
+
@package_assets = case value
|
77
|
+
when 'always' then true
|
78
|
+
when false then false
|
79
|
+
when true then package_env
|
80
|
+
when nil then package_env
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.set_template_function(value)
|
85
|
+
@template_function = case value
|
86
|
+
when false then ''
|
87
|
+
when true then DEFAULT_JST_COMPILER
|
88
|
+
when nil then DEFAULT_JST_COMPILER
|
89
|
+
else value
|
90
|
+
end
|
91
|
+
@include_jst_script = @template_function == DEFAULT_JST_COMPILER
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
# Standard Library Dependencies:
|
97
|
+
require 'zlib'
|
98
|
+
require 'base64'
|
99
|
+
require 'fileutils'
|
100
|
+
|
101
|
+
# Gem Dependencies:
|
102
|
+
require 'rubygems'
|
103
|
+
require 'yui/compressor'
|
104
|
+
require 'activesupport'
|
105
|
+
|
106
|
+
# Load initial configuration before the rest of Jammit.
|
107
|
+
Jammit.load_configuration(Jammit::DEFAULT_CONFIG_PATH)
|
108
|
+
|
109
|
+
# Jammit Core:
|
110
|
+
require 'jammit/compressor'
|
111
|
+
require 'jammit/packager'
|
112
|
+
|
113
|
+
# Jammit Rails Integration:
|
114
|
+
if defined?(RAILS_ENV)
|
115
|
+
require 'jammit/controller' # Rails will auto-load 'jammit/helper' for us.
|
116
|
+
require 'jammit/routes'
|
117
|
+
end
|
@@ -0,0 +1,74 @@
|
|
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_images", 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.precache_all(@options[:output_folder], @options[:base_url])
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Make sure that we have a readable configuration file. The @jammit@
|
37
|
+
# command can't run without one.
|
38
|
+
def ensure_configuration_file
|
39
|
+
config = @options[:config_path]
|
40
|
+
return true if File.exists?(config) && File.readable?(config)
|
41
|
+
puts "Could not find the asset configuration file \"#{config}\""
|
42
|
+
exit(1)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Uses @OptionParser@ to grab the options: *--output*, *--config*, and
|
46
|
+
# *--base-url*...
|
47
|
+
def parse_options
|
48
|
+
@options = {
|
49
|
+
:config_path => Jammit::DEFAULT_CONFIG_PATH,
|
50
|
+
:output_folder => nil,
|
51
|
+
:base_url => nil
|
52
|
+
}
|
53
|
+
@option_parser = OptionParser.new do |opts|
|
54
|
+
opts.on('-o', '--output PATH', 'output folder for packages (default: "public/assets")') do |output_folder|
|
55
|
+
@options[:output_folder] = output_folder
|
56
|
+
end
|
57
|
+
opts.on('-c', '--config PATH', 'path to assets.yml (default: "config/assets.yml")') do |config_path|
|
58
|
+
@options[:config_path] = config_path
|
59
|
+
end
|
60
|
+
opts.on('-u', '--base-url URL', 'base URL for MHTML (ex: "http://example.com")') do |base_url|
|
61
|
+
@options[:base_url] = base_url
|
62
|
+
end
|
63
|
+
opts.on_tail('-v', '--version', 'display Jammit version') do
|
64
|
+
puts "Jammit version #{Jammit::VERSION}"
|
65
|
+
exit
|
66
|
+
end
|
67
|
+
end
|
68
|
+
@option_parser.banner = BANNER
|
69
|
+
@option_parser.parse!(ARGV)
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module Jammit
|
2
|
+
|
3
|
+
# Uses the YUI Compressor to compress JavaScript and CSS. (Which means that
|
4
|
+
# Java must be installed.) Also knows how to create a concatenated JST file.
|
5
|
+
# If "embed_images" is turned on, creates "mhtml" and "datauri" versions of
|
6
|
+
# all stylesheets, with all enabled images inlined into the css.
|
7
|
+
class Compressor
|
8
|
+
|
9
|
+
# Mapping from extension to mime-type of all embeddable images.
|
10
|
+
IMAGE_MIME_TYPES = {
|
11
|
+
'.png' => 'image/png',
|
12
|
+
'.jpg' => 'image/jpeg',
|
13
|
+
'.jpeg' => 'image/jpeg',
|
14
|
+
'.gif' => 'image/gif',
|
15
|
+
'.tif' => 'image/tiff',
|
16
|
+
'.tiff' => 'image/tiff'
|
17
|
+
}
|
18
|
+
|
19
|
+
# Detect all image URLs that are inside of an "embed" folder.
|
20
|
+
IMAGE_DETECTOR = /url\(['"]?(\/[^\s)]*embed\/[^\s)]+\.(png|jpg|jpeg|gif|tif|tiff))['"]?\)/
|
21
|
+
|
22
|
+
# MHTML file constants.
|
23
|
+
MHTML_START = "/*\r\nContent-Type: multipart/related; boundary=\"JAMMIT_MHTML_SEPARATOR\"\r\n\r\n"
|
24
|
+
MHTML_SEPARATOR = "--JAMMIT_MHTML_SEPARATOR\r\n"
|
25
|
+
MHTML_END = "*/\r\n"
|
26
|
+
|
27
|
+
# JST file constants.
|
28
|
+
JST_START = "(function(){window.JST = window.JST || {};"
|
29
|
+
JST_END = "})();"
|
30
|
+
|
31
|
+
# Creating a compressor initializes the internal YUI Compressor from
|
32
|
+
# the "yui-compressor" gem.
|
33
|
+
def initialize
|
34
|
+
@yui_js = YUI::JavaScriptCompressor.new(:munge => true)
|
35
|
+
@yui_css = YUI::CssCompressor.new
|
36
|
+
end
|
37
|
+
|
38
|
+
# Concatenate together a list of JavaScript paths, and pass them through the
|
39
|
+
# YUI Compressor (with munging enabled).
|
40
|
+
def compress_js(paths)
|
41
|
+
@yui_js.compress(concatenate(paths))
|
42
|
+
end
|
43
|
+
|
44
|
+
# Concatenate and compress a list of CSS stylesheets. When compressing a
|
45
|
+
# :datauri or :mhtml variant, post-processes the result to embed
|
46
|
+
# referenced images.
|
47
|
+
def compress_css(paths, variant=nil, asset_url=nil)
|
48
|
+
compressed_css = @yui_css.compress(concatenate(paths))
|
49
|
+
case variant
|
50
|
+
when nil then compressed_css
|
51
|
+
when :datauri then with_data_uris(compressed_css)
|
52
|
+
when :mhtml then with_mhtml(compressed_css, asset_url)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Compiles a single JST file by writing out a javascript that adds
|
57
|
+
# template properties to a top-level "window.JST" object. Adds a
|
58
|
+
# JST-compilation function to the top of the package, unless you've
|
59
|
+
# specified your own preferred function, or turned it off.
|
60
|
+
# JST templates are named with the basename of their file.
|
61
|
+
def compile_jst(paths)
|
62
|
+
compiled = paths.map do |path|
|
63
|
+
template_name = File.basename(path, File.extname(path))
|
64
|
+
contents = File.read(path).gsub(/\n/, '').gsub("'", '\\\\\'')
|
65
|
+
"window.JST.#{template_name} = #{Jammit.template_function}('#{contents}');"
|
66
|
+
end
|
67
|
+
compiler = Jammit.include_jst_script ? File.read(DEFAULT_JST_SCRIPT) : '';
|
68
|
+
[JST_START, compiler, compiled, JST_END].flatten.join("\n")
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# Re-write all enabled image URLs in a stylesheet with their corresponding
|
75
|
+
# Data-URI Base-64 encoded image contents.
|
76
|
+
def with_data_uris(css)
|
77
|
+
css.gsub(IMAGE_DETECTOR) do |url|
|
78
|
+
image_path = "public#{$1}"
|
79
|
+
valid_image(image_path) ? "url(\"data:#{mime_type(image_path)};base64,#{encoded_contents(image_path)}\")" : url
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Re-write all enabled image URLs in a stylesheet with the MHTML equivalent.
|
84
|
+
# The newlines ("\r\n") in the following method are critical. Without them
|
85
|
+
# your MHTML will look identical, but won't work.
|
86
|
+
def with_mhtml(css, asset_url)
|
87
|
+
paths = {}
|
88
|
+
css = css.gsub(IMAGE_DETECTOR) do |url|
|
89
|
+
image_path = "public#{$1}"
|
90
|
+
valid = valid_image(image_path)
|
91
|
+
paths[$1] ||= image_path if valid
|
92
|
+
valid ? "url(mhtml:#{asset_url}!#{$1})" : url
|
93
|
+
end
|
94
|
+
mhtml = paths.map do |identifier, path|
|
95
|
+
mime, contents = mime_type(path), encoded_contents(path)
|
96
|
+
[MHTML_SEPARATOR, "Content-Location: #{identifier}\r\n", "Content-Type: #{mime}\r\n", "Content-Transfer-Encoding: base64\r\n\r\n", contents, "\r\n"]
|
97
|
+
end
|
98
|
+
[MHTML_START, mhtml, MHTML_END, css].flatten.join('')
|
99
|
+
end
|
100
|
+
|
101
|
+
# An image is valid if it exists, and is less than 32K.
|
102
|
+
# IE does not support Data-URIs larger than 32K, and you probably shouldn't
|
103
|
+
# be embedding images that large in any case.
|
104
|
+
def valid_image(image_path)
|
105
|
+
File.exists?(image_path) && File.size(image_path) < 32.kilobytes
|
106
|
+
end
|
107
|
+
|
108
|
+
# Return the Base64-encoded contents of an image on a single line.
|
109
|
+
def encoded_contents(image_path)
|
110
|
+
Base64.encode64(File.read(image_path)).gsub(/\n/, '')
|
111
|
+
end
|
112
|
+
|
113
|
+
# Grab the mime-type of an image, by filename.
|
114
|
+
def mime_type(image_path)
|
115
|
+
IMAGE_MIME_TYPES[File.extname(image_path)]
|
116
|
+
end
|
117
|
+
|
118
|
+
# Concatenate together a list of asset files.
|
119
|
+
def concatenate(paths)
|
120
|
+
[paths].flatten.map {|p| File.read(p) }.join("\n")
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Jammit
|
2
|
+
|
3
|
+
# The JammitController is added to your Rails application when the Gem is
|
4
|
+
# loaded. It takes responsibility for /assets, and dynamically packages any
|
5
|
+
# missing or uncached asset packages.
|
6
|
+
class Controller < ActionController::Base
|
7
|
+
|
8
|
+
VALID_FORMATS = [:css, :js, :jst]
|
9
|
+
|
10
|
+
SUFFIX_STRIPPER = /-(datauri|mhtml)\Z/
|
11
|
+
|
12
|
+
NOT_FOUND_PATH = "#{RAILS_ROOT}/public/404.html"
|
13
|
+
|
14
|
+
after_filter :cache_package if perform_caching
|
15
|
+
|
16
|
+
# The "package" action receives all requests for asset packages that haven't
|
17
|
+
# yet been cached. The package will be built, cached, and gzipped.
|
18
|
+
def package
|
19
|
+
parse_request
|
20
|
+
case @extension
|
21
|
+
when :js then render :js => Jammit.packager.pack_javascripts(@package)
|
22
|
+
when :css then render :text => generate_stylesheets, :content_type => 'text/css'
|
23
|
+
when :jst then render :js => Jammit.packager.pack_templates(@package)
|
24
|
+
end
|
25
|
+
rescue Jammit::PackageNotFound
|
26
|
+
package_not_found
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Tells the Jammit::Packager to cache and gzip an asset package. We can't
|
33
|
+
# just use the built-in "cache_page" because we need to ensure that
|
34
|
+
# the timestamp that ends up in the MHTML is also on the cached file.
|
35
|
+
def cache_package
|
36
|
+
dir = File.join(page_cache_directory, Jammit.package_path)
|
37
|
+
Jammit.packager.cache(@package, @extension, @contents || response.body, dir, @variant, @mtime)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Generate the complete, timestamped, MHTML url -- if we're rendering a
|
41
|
+
# dynamic MHTML package, we'll need to put one URL in the response, and a
|
42
|
+
# different one into the cached package.
|
43
|
+
def prefix_url(path)
|
44
|
+
host = request.port == 80 ? request.host : request.host_with_port
|
45
|
+
"#{request.protocol}#{host}#{path}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# If we're generating MHTML/CSS, return a stylesheet with the absolute
|
49
|
+
# request URL to the client, and cache a version with the timestamped cache
|
50
|
+
# URL swapped in.
|
51
|
+
def generate_stylesheets
|
52
|
+
return Jammit.packager.pack_stylesheets(@package, @variant) unless @variant == :mhtml
|
53
|
+
@mtime = Time.now
|
54
|
+
request_url = prefix_url(request.request_uri)
|
55
|
+
cached_url = prefix_url(Jammit.asset_url(@package, @extension, @variant, @mtime))
|
56
|
+
css = Jammit.packager.pack_stylesheets(@package, @variant, request_url)
|
57
|
+
@contents = css.gsub(request_url, cached_url) if perform_caching
|
58
|
+
css
|
59
|
+
end
|
60
|
+
|
61
|
+
# Extracts the package name, extension (:css, :js, :jst), and variant
|
62
|
+
# (:datauri, :mhtml) from the incoming URL.
|
63
|
+
def parse_request
|
64
|
+
pack = params[:package]
|
65
|
+
@extension = params[:extension].to_sym
|
66
|
+
raise PackageNotFound unless VALID_FORMATS.include?(@extension)
|
67
|
+
if Jammit.embed_images
|
68
|
+
suffix_match = pack.match(SUFFIX_STRIPPER)
|
69
|
+
@variant = Jammit.embed_images && suffix_match && suffix_match[1].to_sym
|
70
|
+
pack.sub!(SUFFIX_STRIPPER, '')
|
71
|
+
end
|
72
|
+
@package = pack.to_sym
|
73
|
+
end
|
74
|
+
|
75
|
+
# Render the 404 page, if one exists, for any packages that don't.
|
76
|
+
def package_not_found
|
77
|
+
return render(:file => NOT_FOUND_PATH, :status => 404) if File.exists?(NOT_FOUND_PATH)
|
78
|
+
render :text => "<h1>404: \"#{@package}\" asset package not found.</h1>", :status => 404
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
::JammitController = Jammit::Controller
|
86
|
+
|
87
|
+
if RAILS_ENV == 'development'
|
88
|
+
class ApplicationController < ActionController::Base
|
89
|
+
before_filter { Jammit.reload! }
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,66 @@
|
|
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_images 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
|
+
return individual_stylesheets(packages) unless Jammit.package_assets
|
19
|
+
return embedded_image_stylesheets(packages) if Jammit.embed_images
|
20
|
+
return packaged_stylesheets(packages)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Writes out the URL to the bundled and compressed javascript package,
|
24
|
+
# except in development, where it references the individual scripts.
|
25
|
+
def include_javascripts(*packages)
|
26
|
+
tags = packages.map do |pack|
|
27
|
+
Jammit.package_assets ? Jammit.asset_url(pack, :js) : Jammit.packager.individual_urls(pack.to_sym, :js)
|
28
|
+
end
|
29
|
+
javascript_include_tag(tags.flatten)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Writes out the URL to the concatenated and compiled JST file -- we always
|
33
|
+
# have to pre-process it, even in development.
|
34
|
+
def include_templates(*packages)
|
35
|
+
javascript_include_tag(packages.map {|pack| Jammit.asset_url(pack, :jst) })
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# HTML tags, in order, for all of the individual stylesheets.
|
42
|
+
def individual_stylesheets(packages)
|
43
|
+
stylesheet_link_tag(packages.map {|p| Jammit.packager.individual_urls(p.to_sym, :css) }.flatten)
|
44
|
+
end
|
45
|
+
|
46
|
+
# HTML tags for the stylesheet packages.
|
47
|
+
def packaged_stylesheets(packages)
|
48
|
+
stylesheet_link_tag(packages.map {|p| Jammit.asset_url(p, :css) })
|
49
|
+
end
|
50
|
+
|
51
|
+
# HTML tags for the 'datauri', and 'mhtml' versions of the packaged
|
52
|
+
# stylesheets, using conditional comments to load the correct variant.
|
53
|
+
def embedded_image_stylesheets(packages)
|
54
|
+
css_tags = stylesheet_link_tag(packages.map {|p| Jammit.asset_url(p, :css, :datauri) })
|
55
|
+
ie_tags = Jammit.mhtml_enabled ?
|
56
|
+
stylesheet_link_tag(packages.map {|p| Jammit.asset_url(p, :css, :mhtml) }) :
|
57
|
+
packaged_stylesheets(packages)
|
58
|
+
[DATA_URI_START, css_tags, DATA_URI_END, MHTML_START, ie_tags, MHTML_END].join("\n")
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
# Include the Jammit asset helpers in all views, a-la ApplicationHelper.
|
66
|
+
::ActionView::Base.send(:include, Jammit::Helper)
|
data/lib/jammit/jst.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
var template = function(str){var fn = new Function('obj', 'var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push(\''+str.replace(/[\r\t\n]/g, " ").split("<%").join("\t").replace(/((^|%>)[^\t]*)'/g, "$1\r").replace(/\t=(.*?)%>/g, "',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"); return fn;};
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Jammit
|
2
|
+
|
3
|
+
# The Jammit::Packager resolves the configuration file into lists of real
|
4
|
+
# assets that get merged into individual asset packages. Given the compiled
|
5
|
+
# contents of an asset package, the Packager knows how to cache that package
|
6
|
+
# with the correct timestamps.
|
7
|
+
class Packager
|
8
|
+
|
9
|
+
# In Rails, the difference between a path and an asset URL is "public".
|
10
|
+
PATH_TO_URL = /\A\/?public/
|
11
|
+
|
12
|
+
# Creating a new Packager will rebuild the list of assets from the
|
13
|
+
# Jammit.configuration. When assets.yml is being changed on the fly,
|
14
|
+
# create a new Packager.
|
15
|
+
def initialize
|
16
|
+
@compressor = Compressor.new
|
17
|
+
@config = {
|
18
|
+
:css => (Jammit.configuration[:stylesheets] || {}).symbolize_keys,
|
19
|
+
:js => (Jammit.configuration[:javascripts] || {}).symbolize_keys,
|
20
|
+
:jst => (Jammit.configuration[:templates] || {}).symbolize_keys
|
21
|
+
}
|
22
|
+
@packages = {
|
23
|
+
:css => create_packages(@config[:css]),
|
24
|
+
:js => create_packages(@config[:js]),
|
25
|
+
:jst => create_packages(@config[:jst])
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
# Ask the packager to precache all defined assets, along with their gzip'd
|
30
|
+
# versions. In order to prebuild the MHTML stylesheets, we need to know the
|
31
|
+
# base_url, because IE only supports MHTML with absolute references.
|
32
|
+
def precache_all(output_dir=nil, base_url=nil)
|
33
|
+
output_dir ||= "public/#{Jammit.package_path}"
|
34
|
+
@config[:js].keys.each {|p| cache(p, 'js', pack_javascripts(p), output_dir) }
|
35
|
+
@config[:jst].keys.each {|p| cache(p, 'jst', pack_templates(p), output_dir) }
|
36
|
+
@config[:css].keys.each do |p|
|
37
|
+
cache(p, 'css', pack_stylesheets(p), output_dir)
|
38
|
+
if Jammit.embed_images
|
39
|
+
cache(p, 'css', pack_stylesheets(p, :datauri), output_dir, :datauri)
|
40
|
+
if Jammit.mhtml_enabled && base_url
|
41
|
+
mtime = Time.now
|
42
|
+
asset_url = "#{base_url}#{Jammit.asset_url(p, :css, :mhtml, mtime)}"
|
43
|
+
cache(p, 'css', pack_stylesheets(p, :mhtml, asset_url), output_dir, :mhtml, mtime)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Caches a single prebuilt asset package and gzips it at the highest
|
50
|
+
# compression level. Ensures that the modification time of both both
|
51
|
+
# variants is identical, for web server caching modules, as well as MHTML.
|
52
|
+
def cache(package, extension, contents, output_dir, suffix=nil, mtime=Time.now)
|
53
|
+
FileUtils.mkdir_p(output_dir) unless File.exists?(output_dir)
|
54
|
+
filename = File.join(output_dir, Jammit.filename(package, extension, suffix))
|
55
|
+
zip_name = "#{filename}.gz"
|
56
|
+
File.open(filename, 'wb+') {|f| f.write(contents) }
|
57
|
+
Zlib::GzipWriter.open(zip_name, Zlib::BEST_COMPRESSION) {|f| f.write(contents) }
|
58
|
+
File.utime(mtime, mtime, filename, zip_name)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get the list of individual assets for a package.
|
62
|
+
def individual_urls(package, extension)
|
63
|
+
package_for(package, extension)[:urls]
|
64
|
+
end
|
65
|
+
|
66
|
+
# Return the compressed contents of a stylesheet package.
|
67
|
+
def pack_stylesheets(package, variant=nil, asset_url=nil)
|
68
|
+
@compressor.compress_css(package_for(package, :css)[:paths], variant, asset_url)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Return the compressed contents of a javascript package.
|
72
|
+
def pack_javascripts(package)
|
73
|
+
@compressor.compress_js(package_for(package, :js)[:paths])
|
74
|
+
end
|
75
|
+
|
76
|
+
# Return the compiled contents of a JST package.
|
77
|
+
def pack_templates(package)
|
78
|
+
@compressor.compile_jst(package_for(package, :jst)[:paths])
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Look up a package asset list by name, raising an exception if the
|
85
|
+
# package has gone missing.
|
86
|
+
def package_for(package, extension)
|
87
|
+
pack = @packages[extension] && @packages[extension][package]
|
88
|
+
pack || not_found(package, extension)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Compiles the list of assets that goes into each package. Runs an ordered
|
92
|
+
# list of Dir.globs, taking the merged unique result.
|
93
|
+
def create_packages(config)
|
94
|
+
packages = {}
|
95
|
+
return packages if !config
|
96
|
+
config.each do |name, globs|
|
97
|
+
globs ||= []
|
98
|
+
packages[name] = {}
|
99
|
+
paths = globs.map {|glob| Dir[glob] }.flatten.uniq
|
100
|
+
packages[name][:paths] = paths
|
101
|
+
packages[name][:urls] = paths.map {|path| path.sub(PATH_TO_URL, '') }
|
102
|
+
end
|
103
|
+
packages
|
104
|
+
end
|
105
|
+
|
106
|
+
# Raise a PackageNotFound exception for missing packages...
|
107
|
+
def not_found(package, extension)
|
108
|
+
raise PackageNotFound, "assets.yml does not contain a \"#{package}\" #{extension.to_s.upcase} package"
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Jammit
|
2
|
+
|
3
|
+
module Routes
|
4
|
+
|
5
|
+
# Jammit uses a single route in order to slow down Rails' routing speed
|
6
|
+
# by the absolute minimum. In your config/routes.rb file, call:
|
7
|
+
# Jammit::Routes.draw(map)
|
8
|
+
# Passing in the routing "map" object.
|
9
|
+
def self.draw(map)
|
10
|
+
map.jammit "/#{Jammit.package_path}/:package.:extension",
|
11
|
+
:controller => 'jammit', :action => 'package'
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jammit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeremy Ashkenas
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-11-06 00:00:00 -05:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rails
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.0.0
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: yui-compressor
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.9.1
|
34
|
+
version:
|
35
|
+
description: " Jammit is an industrial strength asset packaging library for Rails,\n providing both the CSS and JavaScript concatenation and compression\n that you'd expect, as well as ahead-of-time gzipping, built-in JavaScript\n template support, and optional Data-URI / MHTML image embedding.\n"
|
36
|
+
email: jeremy@documentcloud.org
|
37
|
+
executables:
|
38
|
+
- jammit
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- README
|
43
|
+
files:
|
44
|
+
- bin/jammit
|
45
|
+
- jammit.gemspec
|
46
|
+
- lib/jammit.rb
|
47
|
+
- lib/jammit/command_line.rb
|
48
|
+
- lib/jammit/compressor.rb
|
49
|
+
- lib/jammit/controller.rb
|
50
|
+
- lib/jammit/helper.rb
|
51
|
+
- lib/jammit/jst.js
|
52
|
+
- lib/jammit/packager.rb
|
53
|
+
- lib/jammit/routes.rb
|
54
|
+
- LICENSE
|
55
|
+
- Rakefile
|
56
|
+
- README
|
57
|
+
has_rdoc: true
|
58
|
+
homepage: http://documentcloud.github.com/jammit/
|
59
|
+
licenses: []
|
60
|
+
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options:
|
63
|
+
- --title
|
64
|
+
- Jammit
|
65
|
+
- --exclude
|
66
|
+
- test
|
67
|
+
- --main
|
68
|
+
- README
|
69
|
+
- --all
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: "0"
|
77
|
+
version:
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: "0"
|
83
|
+
version:
|
84
|
+
requirements: []
|
85
|
+
|
86
|
+
rubyforge_project: jammit
|
87
|
+
rubygems_version: 1.3.5
|
88
|
+
signing_key:
|
89
|
+
specification_version: 3
|
90
|
+
summary: Industrial Strength Asset Packaging for Rails
|
91
|
+
test_files: []
|
92
|
+
|