ktheory-juicer 1.0.0.ktheory1
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/History.txt +30 -0
- data/Manifest.txt +58 -0
- data/Rakefile +96 -0
- data/Readme.rdoc +312 -0
- data/VERSION +1 -0
- data/bin/juicer +8 -0
- data/lib/juicer.rb +70 -0
- data/lib/juicer/asset/path.rb +275 -0
- data/lib/juicer/asset/path_resolver.rb +79 -0
- data/lib/juicer/binary.rb +171 -0
- data/lib/juicer/cache_buster.rb +130 -0
- data/lib/juicer/chainable.rb +106 -0
- data/lib/juicer/cli.rb +56 -0
- data/lib/juicer/command/install.rb +61 -0
- data/lib/juicer/command/list.rb +57 -0
- data/lib/juicer/command/merge.rb +205 -0
- data/lib/juicer/command/util.rb +32 -0
- data/lib/juicer/command/verify.rb +60 -0
- data/lib/juicer/css_cache_buster.rb +80 -0
- data/lib/juicer/datafy/datafy.rb +20 -0
- data/lib/juicer/dependency_resolver/css_dependency_resolver.rb +29 -0
- data/lib/juicer/dependency_resolver/dependency_resolver.rb +101 -0
- data/lib/juicer/dependency_resolver/javascript_dependency_resolver.rb +23 -0
- data/lib/juicer/ext/logger.rb +5 -0
- data/lib/juicer/ext/string.rb +47 -0
- data/lib/juicer/ext/symbol.rb +15 -0
- data/lib/juicer/image_embed.rb +136 -0
- data/lib/juicer/install/base.rb +186 -0
- data/lib/juicer/install/closure_compiler_installer.rb +69 -0
- data/lib/juicer/install/jslint_installer.rb +51 -0
- data/lib/juicer/install/rhino_installer.rb +53 -0
- data/lib/juicer/install/yui_compressor_installer.rb +67 -0
- data/lib/juicer/jslint.rb +90 -0
- data/lib/juicer/merger/base.rb +74 -0
- data/lib/juicer/merger/javascript_merger.rb +29 -0
- data/lib/juicer/merger/stylesheet_merger.rb +110 -0
- data/lib/juicer/minifyer/closure_compiler.rb +90 -0
- data/lib/juicer/minifyer/java_base.rb +77 -0
- data/lib/juicer/minifyer/yui_compressor.rb +96 -0
- data/test/bin/jslint-1.0.js +523 -0
- data/test/bin/jslint.js +523 -0
- data/test/bin/rhino1_7R1.zip +0 -0
- data/test/bin/rhino1_7R2-RC1.jar +0 -0
- data/test/bin/rhino1_7R2-RC1.zip +0 -0
- data/test/bin/yuicompressor +0 -0
- data/test/bin/yuicompressor-2.3.5.zip +0 -0
- data/test/bin/yuicompressor-2.4.2.jar +0 -0
- data/test/bin/yuicompressor-2.4.2.zip +0 -0
- data/test/data/Changelog.txt +10 -0
- data/test/data/a.css +3 -0
- data/test/data/a.js +5 -0
- data/test/data/a1.css +5 -0
- data/test/data/b.css +1 -0
- data/test/data/b.js +5 -0
- data/test/data/b1.css +5 -0
- data/test/data/c1.css +3 -0
- data/test/data/css/2.gif +1 -0
- data/test/data/css/test.css +11 -0
- data/test/data/css/test2.css +1 -0
- data/test/data/d1.css +3 -0
- data/test/data/images/1.png +1 -0
- data/test/data/my_app.js +2 -0
- data/test/data/not-ok.js +2 -0
- data/test/data/ok.js +3 -0
- data/test/data/path_test.css +5 -0
- data/test/data/path_test2.css +14 -0
- data/test/data/pkg/module/moda.js +2 -0
- data/test/data/pkg/module/modb.js +3 -0
- data/test/data/pkg/pkg.js +1 -0
- data/test/test_helper.rb +169 -0
- data/test/unit/juicer/asset/path_resolver_test.rb +76 -0
- data/test/unit/juicer/asset/path_test.rb +370 -0
- data/test/unit/juicer/cache_buster_test.rb +104 -0
- data/test/unit/juicer/chainable_test.rb +94 -0
- data/test/unit/juicer/command/install_test.rb +58 -0
- data/test/unit/juicer/command/list_test.rb +81 -0
- data/test/unit/juicer/command/merge_test.rb +162 -0
- data/test/unit/juicer/command/util_test.rb +58 -0
- data/test/unit/juicer/command/verify_test.rb +48 -0
- data/test/unit/juicer/css_cache_buster_test.rb +71 -0
- data/test/unit/juicer/datafy_test.rb +37 -0
- data/test/unit/juicer/dependency_resolver/css_dependency_resolver_test.rb +36 -0
- data/test/unit/juicer/dependency_resolver/javascript_dependency_resolver_test.rb +50 -0
- data/test/unit/juicer/ext/string_test.rb +59 -0
- data/test/unit/juicer/ext/symbol_test.rb +27 -0
- data/test/unit/juicer/image_embed_test.rb +271 -0
- data/test/unit/juicer/install/installer_base_test.rb +214 -0
- data/test/unit/juicer/install/jslint_installer_test.rb +54 -0
- data/test/unit/juicer/install/rhino_installer_test.rb +57 -0
- data/test/unit/juicer/install/yui_compressor_test.rb +56 -0
- data/test/unit/juicer/jslint_test.rb +60 -0
- data/test/unit/juicer/merger/base_test.rb +122 -0
- data/test/unit/juicer/merger/javascript_merger_test.rb +74 -0
- data/test/unit/juicer/merger/stylesheet_merger_test.rb +180 -0
- data/test/unit/juicer/minifyer/closure_compressor_test.rb +107 -0
- data/test/unit/juicer/minifyer/yui_compressor_test.rb +116 -0
- data/test/unit/juicer_test.rb +1 -0
- metadata +265 -0
@@ -0,0 +1,130 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Juicer
|
4
|
+
#
|
5
|
+
# Assists in creating filenames that reflect the last change to the file. These
|
6
|
+
# kinds of filenames are useful when serving static content through a web server.
|
7
|
+
# If the filename changes everytime the file is modified, you can safely configure
|
8
|
+
# the web server to cache files indefinately, and know that the updated filename
|
9
|
+
# will cause the file to be downloaded again - only once - when it has changed.
|
10
|
+
#
|
11
|
+
# = Types of cache busters
|
12
|
+
#
|
13
|
+
# == Query string / "soft" cache busters
|
14
|
+
# Soft cache busters require no web server configuration. However, it is not
|
15
|
+
# guaranteed to work in all settings. For example, older default
|
16
|
+
# configurations for popular proxy server Squid does not consider a known URL
|
17
|
+
# with a new query string a new URL, and thus will not download the file over.
|
18
|
+
#
|
19
|
+
# The soft cache busters transforms
|
20
|
+
# <tt>/images/logo.png</tt> to <tt>/images/logo.png?cb=1232923789</tt>
|
21
|
+
#
|
22
|
+
# == Filename change / "hard" cache busters
|
23
|
+
# Hard cache busters change the file name itself, and thus requires either
|
24
|
+
# the web server to (internally) rewrite requests for these files to the
|
25
|
+
# original ones, or the file names to actually change. Hard cache busters
|
26
|
+
# transforms <tt>/images/logo.png</tt> to <tt>/images/logo-1232923789.png</tt>
|
27
|
+
#
|
28
|
+
# Hard cache busters are guaranteed to work, and is the recommended variant.
|
29
|
+
# An example configuration for the Apache web server that does not require
|
30
|
+
# you to actually change the filenames can be seen below.
|
31
|
+
#
|
32
|
+
# <VirtualHost *>
|
33
|
+
# # Application/website configuration
|
34
|
+
#
|
35
|
+
# # Cache static resources for a year
|
36
|
+
# <FilesMatch "\.(ico|pdf|flv|jpg|jpeg|png|gif|js|css|swf)$">
|
37
|
+
# ExpiresActive On
|
38
|
+
# ExpiresDefault "access plus 1 year"
|
39
|
+
# </FilesMatch>
|
40
|
+
#
|
41
|
+
# # Rewrite URLs like /images/logo-cb1234567890.png to /images/logo.png
|
42
|
+
# RewriteEngine On
|
43
|
+
# RewriteRule (.*)-cb\d+\.(.*)$ $1.$2 [L]
|
44
|
+
# </VirtualHost>])
|
45
|
+
#
|
46
|
+
# = Consecutive calls
|
47
|
+
#
|
48
|
+
# Consecutive calls to add a cache buster to a path will replace the existing
|
49
|
+
# cache buster *as long as the parameter name is the same*. Consider this:
|
50
|
+
#
|
51
|
+
# file = Juicer::CacheBuster.hard("/home/file.png") #=> "/home/file-cb1234567890.png"
|
52
|
+
# Juicer::CacheBuster.hard(file) #=> "/home/file-cb1234567891.png"
|
53
|
+
#
|
54
|
+
# # Changing the parameter name breaks this
|
55
|
+
# Juicer::CacheBuster.hard(file, :juicer) #=> "/home/file-cb1234567891-juicer1234567892.png"
|
56
|
+
#
|
57
|
+
# Avoid this type of trouble simply be cleaning the URL with the old name first:
|
58
|
+
#
|
59
|
+
# Juicer::CacheBuster.clean(file) #=> "/home/file.png"
|
60
|
+
# file = Juicer::CacheBuster.hard(file, :juicer) #=> "/home/file-juicer1234567892.png"
|
61
|
+
# Juicer::CacheBuster.clean(file, :juicer) #=> "/home/file.png"
|
62
|
+
#
|
63
|
+
# Author:: Christian Johansen (christian@cjohansen.no)
|
64
|
+
# Copyright:: Copyright (c) 2009 Christian Johansen
|
65
|
+
# License:: BSD
|
66
|
+
#
|
67
|
+
module CacheBuster
|
68
|
+
DEFAULT_PARAMETER = "jcb"
|
69
|
+
|
70
|
+
#
|
71
|
+
# Creates a unique file name for every revision to the files contents.
|
72
|
+
# Raises an <tt>ArgumentError</tt> if the file can not be found.
|
73
|
+
#
|
74
|
+
# The type indicates which type of cache buster you want, <tt>:soft</tt>
|
75
|
+
# or <tt>:hard</tt>. Default is <tt>:soft</tt>. If an unsupported value
|
76
|
+
# is specified, <tt>:soft</tt> will be used.
|
77
|
+
#
|
78
|
+
# See <tt>#hard</tt> and <tt>#soft</tt> for explanation of the parameter
|
79
|
+
# argument.
|
80
|
+
#
|
81
|
+
def self.path(file, type = :soft, parameter = DEFAULT_PARAMETER)
|
82
|
+
file = self.clean(file, parameter)
|
83
|
+
filename = file.split("?").first
|
84
|
+
raise ArgumentError.new("#{file} could not be found") unless File.exists?(filename)
|
85
|
+
mtime = File.mtime(filename).to_i
|
86
|
+
type = [:soft, :hard].include?(type) ? type : :soft
|
87
|
+
|
88
|
+
if type == :soft
|
89
|
+
parameter = "#{parameter}=".sub(/^=$/, '')
|
90
|
+
return "#{file}#{file.index('?') ? '&' : '?'}#{parameter}#{mtime}"
|
91
|
+
end
|
92
|
+
|
93
|
+
file.sub(/(\.[^\.]+$)/, "-#{parameter}#{mtime}" + '\1')
|
94
|
+
end
|
95
|
+
|
96
|
+
#
|
97
|
+
# Add a hard cache buster to a filename. The parameter is an optional prefix
|
98
|
+
# that is added before the mtime timestamp. It results in filenames of the form:
|
99
|
+
# <tt>file-[parameter name][timestamp].suffix</tt>, ie
|
100
|
+
# <tt>images/logo-cb1234567890.png</tt> which is the case for the default
|
101
|
+
# parameter name "cb" (as in *c*ache *b*uster).
|
102
|
+
#
|
103
|
+
def self.hard(file, parameter = DEFAULT_PARAMETER)
|
104
|
+
self.path(file, :hard, parameter)
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Add a soft cache buster to a filename. The parameter is an optional name
|
109
|
+
# for the mtime timestamp value. It results in filenames of the form:
|
110
|
+
# <tt>file.suffix?[parameter name]=[timestamp]</tt>, ie
|
111
|
+
# <tt>images/logo.png?cb=1234567890</tt> which is the case for the default
|
112
|
+
# parameter name "cb" (as in *c*ache *b*uster).
|
113
|
+
#
|
114
|
+
def self.soft(file, parameter = DEFAULT_PARAMETER)
|
115
|
+
self.path(file, :soft, parameter)
|
116
|
+
end
|
117
|
+
|
118
|
+
#
|
119
|
+
# Remove cache buster from a URL for a given parameter name. Parameter name is
|
120
|
+
# "cb" by default.
|
121
|
+
#
|
122
|
+
def self.clean(file, parameter = DEFAULT_PARAMETER)
|
123
|
+
query_param = "#{parameter}".length == 0 ? "" : "#{parameter}="
|
124
|
+
new_file = file.sub(/#{query_param}\d+&?/, "").sub(/(\?|&)$/, "")
|
125
|
+
return new_file unless new_file == file
|
126
|
+
|
127
|
+
file.sub(/-#{parameter}\d+(\.\w+)($|\?)/, '\1\2')
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Juicer
|
2
|
+
#
|
3
|
+
# Facilitates the chain of responsibility pattern. Wraps given methods and
|
4
|
+
# calls them in a chain.
|
5
|
+
#
|
6
|
+
# To make an object chainable, simply include the module and call the class
|
7
|
+
# method chain_method for each method that should be chained.
|
8
|
+
#
|
9
|
+
# Example is a simplified version of the Wikipedia one
|
10
|
+
# (http://en.wikipedia.org/wiki/Chain-of-responsibility_pattern)
|
11
|
+
#
|
12
|
+
# class Logger
|
13
|
+
# include Juicer::Chainable
|
14
|
+
#
|
15
|
+
# ERR = 3
|
16
|
+
# NOTICE = 5
|
17
|
+
# DEBUG = 7
|
18
|
+
#
|
19
|
+
# def initialize(level)
|
20
|
+
# @level = level
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# def log(str, level)
|
24
|
+
# if level <= @level
|
25
|
+
# write str
|
26
|
+
# else
|
27
|
+
# abort_chain
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# def write(str)
|
32
|
+
# puts str
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# chain_method :message
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# class EmailLogger < Logger
|
39
|
+
# def write(str)
|
40
|
+
# p "Logging by email"
|
41
|
+
# # ...
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# logger = Logger.new(Logger::NOTICE)
|
46
|
+
# logger.next_in_chain = EmailLogger.new(Logger::ERR)
|
47
|
+
#
|
48
|
+
# logger.log("Some message", Logger::DEBUG) # Ignored
|
49
|
+
# logger.log("A warning", Logger::NOTICE) # Logged to console
|
50
|
+
# logger.log("An error", Logger::ERR) # Logged to console and email
|
51
|
+
#
|
52
|
+
module Chainable
|
53
|
+
|
54
|
+
#
|
55
|
+
# Add the chain_method to classes that includes the module
|
56
|
+
#
|
57
|
+
def self.included(base)
|
58
|
+
base.extend(ClassMethods)
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Sets the next command in the chain
|
63
|
+
#
|
64
|
+
def next_in_chain=(next_obj)
|
65
|
+
@_next_in_chain = next_obj
|
66
|
+
next_obj || self
|
67
|
+
end
|
68
|
+
|
69
|
+
alias_method :set_next, :next_in_chain=
|
70
|
+
|
71
|
+
#
|
72
|
+
# Get next command in chain
|
73
|
+
#
|
74
|
+
def next_in_chain
|
75
|
+
@_next_in_chain ||= nil
|
76
|
+
@_next_in_chain
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
#
|
81
|
+
# Abort the chain for the current message
|
82
|
+
#
|
83
|
+
def abort_chain
|
84
|
+
@_abort_chain = true
|
85
|
+
end
|
86
|
+
|
87
|
+
module ClassMethods
|
88
|
+
#
|
89
|
+
# Sets up a method for chaining
|
90
|
+
#
|
91
|
+
def chain_method(method)
|
92
|
+
original_method = "execute_#{method}".to_sym
|
93
|
+
alias_method original_method, method
|
94
|
+
|
95
|
+
self.class_eval <<-RUBY
|
96
|
+
def #{method}(*args, &block)
|
97
|
+
@_abort_chain = false
|
98
|
+
#{original_method}(*args, &block)
|
99
|
+
next_in_chain.#{method}(*args, &block) if !@_abort_chain && next_in_chain
|
100
|
+
@_abort_chain = false
|
101
|
+
end
|
102
|
+
RUBY
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/lib/juicer/cli.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require "cmdparse"
|
2
|
+
|
3
|
+
# Command line interpreter for Juicer
|
4
|
+
#
|
5
|
+
module Juicer
|
6
|
+
class Cli
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@log = Juicer::LOGGER
|
10
|
+
@log.level = Logger::INFO
|
11
|
+
end
|
12
|
+
|
13
|
+
# Set up command parser and parse arguments
|
14
|
+
#
|
15
|
+
def parse(arguments = ARGV)
|
16
|
+
@cmd = CmdParse::CommandParser.new(true, true)
|
17
|
+
@cmd.program_name = "juicer"
|
18
|
+
@cmd.program_version = Juicer.version.split(".")
|
19
|
+
|
20
|
+
@cmd.options = CmdParse::OptionParserWrapper.new do |opt|
|
21
|
+
opt.separator "Global options:"
|
22
|
+
opt.on("-v", "--verbose", "Be verbose when outputting info") { |t| @log.level = Logger::DEBUG }
|
23
|
+
opt.on("-q", "--quiet", "Only log warnings and errors") { |t| @log.level = Logger::WARN }
|
24
|
+
end
|
25
|
+
|
26
|
+
add_commands
|
27
|
+
@cmd.parse(arguments)
|
28
|
+
@log.close
|
29
|
+
rescue SystemExit
|
30
|
+
exit
|
31
|
+
end
|
32
|
+
|
33
|
+
# Run CLI
|
34
|
+
#
|
35
|
+
def self.run(arguments = ARGV)
|
36
|
+
juicer = self.new
|
37
|
+
juicer.parse(arguments)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
# Adds commands supported by juicer. Instantiates all classes in the
|
42
|
+
# Juicer::Command namespace.
|
43
|
+
#
|
44
|
+
def add_commands
|
45
|
+
@cmd.add_command(CmdParse::HelpCommand.new)
|
46
|
+
@cmd.add_command(CmdParse::VersionCommand.new)
|
47
|
+
|
48
|
+
if Juicer.const_defined?("Command")
|
49
|
+
Juicer::Command.constants.each do |const|
|
50
|
+
const = Juicer::Command.const_get(const)
|
51
|
+
@cmd.add_command(const.new(@log)) if const.kind_of?(Class)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require "juicer/command/util"
|
2
|
+
require "cmdparse"
|
3
|
+
require "pathname"
|
4
|
+
|
5
|
+
module Juicer
|
6
|
+
module Command
|
7
|
+
# Installs a third party library so Juicer can use it.
|
8
|
+
#
|
9
|
+
class Install < CmdParse::Command
|
10
|
+
include Juicer::Command::Util
|
11
|
+
|
12
|
+
# Initializes command
|
13
|
+
#
|
14
|
+
def initialize(io = nil)
|
15
|
+
super('install', false, true)
|
16
|
+
@io = io || Logger.new(STDOUT)
|
17
|
+
@version = nil
|
18
|
+
@path = Juicer.home
|
19
|
+
self.short_desc = "Install a third party library"
|
20
|
+
self.description = <<-EOF
|
21
|
+
Installs a third party used by Juicer. Downloads necessary binaries and licenses
|
22
|
+
into Juicer installation directory, usually ~/.juicer
|
23
|
+
EOF
|
24
|
+
|
25
|
+
self.options = CmdParse::OptionParserWrapper.new do |opt|
|
26
|
+
opt.on('-v', '--version [VERSION]', 'Specify version of library to install') { |version| @version = version }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Execute command
|
31
|
+
#
|
32
|
+
def execute(*args)
|
33
|
+
args.flatten!
|
34
|
+
|
35
|
+
if args.length == 0
|
36
|
+
raise ArgumentError.new('Please provide a library to install')
|
37
|
+
end
|
38
|
+
|
39
|
+
args.each do |lib|
|
40
|
+
installer = Juicer::Install.get(lib).new(@path)
|
41
|
+
path = File.join(installer.install_dir, installer.path)
|
42
|
+
version = version(installer)
|
43
|
+
|
44
|
+
if installer.installed?(version)
|
45
|
+
@io.info "#{installer.name} #{version} is already installed in #{path}"
|
46
|
+
break
|
47
|
+
end
|
48
|
+
|
49
|
+
installer.install(version)
|
50
|
+
@io.info "Successfully installed #{lib.camel_case} #{version} in #{path}" if installer.installed?(version)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns which version to install
|
55
|
+
#
|
56
|
+
def version(installer)
|
57
|
+
@version ||= installer.latest
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require "juicer/command/util"
|
2
|
+
require "cmdparse"
|
3
|
+
require "pathname"
|
4
|
+
|
5
|
+
module Juicer
|
6
|
+
module Command
|
7
|
+
# Displays a list of files that make up the dependency chain for the input
|
8
|
+
# files/patterns.
|
9
|
+
#
|
10
|
+
class List < CmdParse::Command
|
11
|
+
include Juicer::Command::Util
|
12
|
+
|
13
|
+
# Initializes command
|
14
|
+
#
|
15
|
+
def initialize(log = nil)
|
16
|
+
super('list', false, true)
|
17
|
+
@log = log
|
18
|
+
self.short_desc = "Lists all dependencies for all input files/patterns"
|
19
|
+
self.description = <<-EOF
|
20
|
+
Dependencies are looked up recursively. The dependency chain reveals which files
|
21
|
+
will be joined by juicer merge.
|
22
|
+
|
23
|
+
Input parameters may be:
|
24
|
+
* Single file, ie $ juicer list myfile.css
|
25
|
+
* Single glob pattern, ie $ juicer list **/*.css
|
26
|
+
* Multiple mixed arguments, ie $ juicer list **/*.js **/*.css
|
27
|
+
EOF
|
28
|
+
end
|
29
|
+
|
30
|
+
# Execute command
|
31
|
+
#
|
32
|
+
def execute(args)
|
33
|
+
if args.length == 0
|
34
|
+
raise ArgumentError.new('Please provide atleast one input file/pattern')
|
35
|
+
end
|
36
|
+
|
37
|
+
types = { :js => Juicer::JavaScriptDependencyResolver.new,
|
38
|
+
:css => Juicer::CssDependencyResolver.new }
|
39
|
+
|
40
|
+
result = files(args).map { |file|
|
41
|
+
type = file.split(".").pop.to_sym
|
42
|
+
raise FileNotFoundError.new("Unable to guess type (CSS/JavaScript) of file #{relative(file)}") unless types[type]
|
43
|
+
|
44
|
+
deps = relative types[type].resolve(file)
|
45
|
+
# there may only be one dependency, which resolve() returns as a string
|
46
|
+
deps = deps.join("\n ") if deps.is_a? Array
|
47
|
+
|
48
|
+
"Dependency chain for #{relative file}:\n #{deps}"
|
49
|
+
}.join("\n\n") + "\n"
|
50
|
+
|
51
|
+
@log.info result
|
52
|
+
|
53
|
+
result
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
require "juicer/command/util"
|
2
|
+
require "juicer/command/verify"
|
3
|
+
require "cmdparse"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
module Juicer
|
7
|
+
module Command
|
8
|
+
# The compress command combines and minifies CSS and JavaScript files
|
9
|
+
#
|
10
|
+
class Merge < CmdParse::Command
|
11
|
+
include Juicer::Command::Util
|
12
|
+
|
13
|
+
# Initializes compress command
|
14
|
+
#
|
15
|
+
def initialize(log = nil)
|
16
|
+
super('merge', false, true)
|
17
|
+
@types = { :js => Juicer::Merger::JavaScriptMerger,
|
18
|
+
:css => Juicer::Merger::StylesheetMerger }
|
19
|
+
@output = nil # File to write to
|
20
|
+
@force = false # Overwrite existing file if true
|
21
|
+
@type = nil # "css" or "js" - for minifyer
|
22
|
+
@minifyer = "yui_compressor" # Which minifyer to use
|
23
|
+
@opts = {} # Path to minifyer binary
|
24
|
+
@arguments = nil # Minifyer arguments
|
25
|
+
@ignore = false # Ignore syntax problems if true
|
26
|
+
@cache_buster = :soft # What kind of cache buster to use, :soft or :hard
|
27
|
+
@hosts = nil # Hosts to use when replacing URLs in stylesheets
|
28
|
+
@web_root = nil # Used to understand absolute paths
|
29
|
+
@relative_urls = false # Make the merger use relative URLs
|
30
|
+
@absolute_urls = false # Make the merger use absolute URLs
|
31
|
+
@local_hosts = [] # Host names that are served from :web_root
|
32
|
+
@verify = true # Verify js files with JsLint
|
33
|
+
@image_embed_type = :none # Embed images in css files, options are :none, :data_uri
|
34
|
+
|
35
|
+
@log = log || Logger.new(STDOUT)
|
36
|
+
|
37
|
+
self.short_desc = "Combines and minifies CSS and JavaScript files"
|
38
|
+
self.description = <<-EOF
|
39
|
+
Each file provided as input will be checked for dependencies to other files,
|
40
|
+
and those files will be added to the final output
|
41
|
+
|
42
|
+
For CSS files the dependency checking is done through regular @import
|
43
|
+
statements.
|
44
|
+
|
45
|
+
For JavaScript files you can tell Juicer about dependencies through special
|
46
|
+
comment switches. These should appear inside a multi-line comment, specifically
|
47
|
+
inside the first multi-line comment. The switch is @depend or @depends, your
|
48
|
+
choice.
|
49
|
+
|
50
|
+
The -m --minifyer switch can be used to select which minifyer to use. Currently
|
51
|
+
only YUI Compressor and Google Closure Compiler is supported, ie -m yui_compressor (default) or -m closure_compiler. When using
|
52
|
+
the compressor the path should be the path to where the jar file is found.
|
53
|
+
EOF
|
54
|
+
|
55
|
+
self.options = CmdParse::OptionParserWrapper.new do |opt|
|
56
|
+
opt.on("-o", "--output file", "Output filename") { |filename| @output = filename }
|
57
|
+
opt.on("-p", "--path path", "Path to compressor binary") { |path| @opts[:bin_path] = path }
|
58
|
+
opt.on("-m", "--minifyer name", "Which minifer to use. Currently only supports yui_compressor and closure compiler") { |name| @minifyer = name }
|
59
|
+
opt.on("-f", "--force", "Force overwrite of target file") { @force = true }
|
60
|
+
opt.on("-a", "--arguments arguments", "Arguments to minifyer, escape with quotes") { |arguments|
|
61
|
+
@arguments = arguments.to_s.gsub(/(^['"]|["']$)/, "")
|
62
|
+
}
|
63
|
+
opt.on("-i", "--ignore-problems", "Merge and minify even if verifyer finds problems") { @ignore = true }
|
64
|
+
opt.on("-s", "--skip-verification", "Skip JsLint verification (js files only). Not recomended!") { @verify = false }
|
65
|
+
opt.on("-t", "--type type", "Juicer can only guess type when files have .css or .js extensions. Specify js or\n" +
|
66
|
+
(" " * 37) + "css with this option in cases where files have other extensions.") { |type| @type = type.to_sym }
|
67
|
+
opt.on("-h", "--hosts hosts", "Cycle asset hosts for referenced urls. Comma separated") { |hosts| @hosts = hosts.split(",") }
|
68
|
+
opt.on("-l", "--local-hosts hosts", "Host names that are served from --document-root (can be given cache busters). Comma separated") do |hosts|
|
69
|
+
@local_hosts = hosts.split(",")
|
70
|
+
end
|
71
|
+
opt.on("", "--all-hosts-local", "Treat all hosts as local (ie served from --document-root") { |t| @local_hosts = @hosts }
|
72
|
+
opt.on("-r", "--relative-urls", "Convert all referenced URLs to relative URLs. Requires --document-root if\n" +
|
73
|
+
(" " * 37) + "absolute URLs are used. Only valid for CSS files") { |t| @relative_urls = true }
|
74
|
+
opt.on("-b", "--absolute-urls", "Convert all referenced URLs to absolute URLs. Requires --document-root.\n" +
|
75
|
+
(" " * 37) + "Works with cycled asset hosts. Only valid for CSS files") { |t| @absolute_urls = true }
|
76
|
+
opt.on("-d", "--document-root dir", "Path to resolve absolute URLs relative to") { |path| @web_root = path }
|
77
|
+
opt.on("-c", "--cache-buster type", "none, soft or hard. Default is soft, which adds timestamps to referenced\n" +
|
78
|
+
(" " * 37) + "URLs as query parameters. None leaves URLs untouched and hard alters file names") do |type|
|
79
|
+
@cache_buster = [:soft, :hard].include?(type.to_sym) ? type.to_sym : nil
|
80
|
+
end
|
81
|
+
opt.on("-e", "--embed-images type", "none or data_uri. Default is none. Data_uri embeds images using Base64 encoding\n" +
|
82
|
+
(" " * 37) + "None leaves URLs untouched. Candiate images must be flagged with '?embed=true to be considered") do |embed|
|
83
|
+
@image_embed_type = [:none, :data_uri].include?(embed.to_sym) ? embed.to_sym : nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Execute command
|
89
|
+
#
|
90
|
+
def execute(args)
|
91
|
+
if (files = files(args)).length == 0
|
92
|
+
@log.fatal "Please provide atleast one input file"
|
93
|
+
raise SystemExit.new("Please provide atleast one input file")
|
94
|
+
end
|
95
|
+
|
96
|
+
# Figure out which file to output to
|
97
|
+
output = output(files.first)
|
98
|
+
|
99
|
+
# Warn if file already exists
|
100
|
+
if File.exists?(output) && !@force
|
101
|
+
msg = "Unable to continue, #{output} exists. Run again with --force to overwrite"
|
102
|
+
@log.fatal msg
|
103
|
+
raise SystemExit.new(msg)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Set up merger to resolve imports and so on. Do not touch URLs now, if
|
107
|
+
# asset host cycling is added at this point, the cache buster WILL be
|
108
|
+
# confused
|
109
|
+
merger = merger(output).new(files, :relative_urls => @relative_urls,
|
110
|
+
:absolute_urls => @absolute_urls,
|
111
|
+
:web_root => @web_root,
|
112
|
+
:hosts => @hosts)
|
113
|
+
|
114
|
+
# Fail if syntax trouble (js only)
|
115
|
+
if @verify && !Juicer::Command::Verify.check_all(merger.files.reject { |f| f =~ /\.css$/ }, @log)
|
116
|
+
@log.error "Problems were detected during verification"
|
117
|
+
raise SystemExit.new("Input files contain problems") unless @ignore
|
118
|
+
@log.warn "Ignoring detected problems"
|
119
|
+
end
|
120
|
+
|
121
|
+
# Set command chain and execute
|
122
|
+
merger.set_next(image_embed(output)).set_next(cache_buster(output)).set_next(minifyer)
|
123
|
+
merger.save(output)
|
124
|
+
|
125
|
+
# Print report
|
126
|
+
@log.info "Produced #{relative output} from"
|
127
|
+
merger.files.each { |file| @log.info " #{relative file}" }
|
128
|
+
rescue FileNotFoundError => err
|
129
|
+
# Handle missing document-root option
|
130
|
+
puts err.message.sub(/:web_root/, "--document-root")
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
#
|
135
|
+
# Resolve and load minifyer
|
136
|
+
#
|
137
|
+
def minifyer
|
138
|
+
return nil if @minifyer.nil? || @minifyer == "" || @minifyer.downcase == "none"
|
139
|
+
|
140
|
+
begin
|
141
|
+
@opts[:bin_path] = File.join(Juicer.home, "lib", @minifyer, "bin") unless @opts[:bin_path]
|
142
|
+
compressor = @minifyer.classify(Juicer::Minifyer).new(@opts)
|
143
|
+
compressor.set_opts(@arguments) if @arguments
|
144
|
+
@log.debug "Using #{@minifyer.camel_case} for minification"
|
145
|
+
|
146
|
+
return compressor
|
147
|
+
rescue NameError => e
|
148
|
+
@log.fatal e.message
|
149
|
+
@log.fatal "No such minifyer '#{@minifyer}', aborting"
|
150
|
+
raise SystemExit.new("No such minifyer '#{@minifyer}', aborting")
|
151
|
+
rescue FileNotFoundError => e
|
152
|
+
@log.fatal e.message
|
153
|
+
@log.fatal "Try installing with; juicer install #{@minifyer.underscore}"
|
154
|
+
raise SystemExit.new(e.message)
|
155
|
+
rescue Exception => e
|
156
|
+
@log.fatal e.message
|
157
|
+
raise SystemExit.new(e.message)
|
158
|
+
end
|
159
|
+
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
|
163
|
+
#
|
164
|
+
# Resolve and load merger
|
165
|
+
#
|
166
|
+
def merger(output = "")
|
167
|
+
@type ||= output.split(/\.([^\.]*)$/)[1]
|
168
|
+
type = @type.to_sym if @type
|
169
|
+
|
170
|
+
if !@types.include?(type)
|
171
|
+
@log.warn "Unknown type '#{type}', defaulting to 'js'"
|
172
|
+
type = :js
|
173
|
+
end
|
174
|
+
|
175
|
+
@types[type]
|
176
|
+
end
|
177
|
+
|
178
|
+
#
|
179
|
+
# Load cache buster, only available for CSS files
|
180
|
+
#
|
181
|
+
def cache_buster(file)
|
182
|
+
return nil if !file || file !~ /\.css$/ || @cache_buster.nil?
|
183
|
+
Juicer::CssCacheBuster.new(:web_root => @web_root, :type => @cache_buster, :hosts => @local_hosts)
|
184
|
+
end
|
185
|
+
|
186
|
+
#
|
187
|
+
# Load image embed, only available for CSS files
|
188
|
+
#
|
189
|
+
def image_embed(file)
|
190
|
+
return nil if !file || file !~ /\.css$/ || @image_embed_type.nil?
|
191
|
+
Juicer::ImageEmbed.new( :type => @image_embed_type )
|
192
|
+
end
|
193
|
+
|
194
|
+
#
|
195
|
+
# Generate output file name. Optional argument is a filename to base the new
|
196
|
+
# name on. It will prepend the original suffix with ".min"
|
197
|
+
#
|
198
|
+
def output(file = "#{Time.now.to_i}.tmp")
|
199
|
+
@output = File.dirname(file) if @output.nil?
|
200
|
+
@output = File.join(@output, File.basename(file).sub(/\.([^\.]+)$/, '.min.\1')) if File.directory?(@output)
|
201
|
+
@output = File.expand_path(@output)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|