jammit 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|