Created
February 17, 2012 02:02
-
-
Save vdh/1849803 to your computer and use it in GitHub Desktop.
A fairly simple JS packaging script using UglifyJS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Copyright (c) 2011 Tim van der Horst. Licensed under the MIT License: | |
# http://www.opensource.org/licenses/mit-license.php | |
# UglifyJS: | |
# https://github.com/mishoo/UglifyJS | |
require "rubygems" | |
require "optparse" | |
require "date" | |
require "stringio" | |
require "digest/md5" | |
require "json" | |
#require "uglifier" # (used further down...) | |
class JSPackage | |
VERSION = "2.0.3" | |
RESET = "0" | |
BOLD = "1" | |
BOLD_RESET = "22" | |
RED = "0;31" | |
GREEN = "0;32" | |
OLIVE = "0;33" | |
GREY = "1;30" | |
SPACER = "." | |
COPYRIGHT_FILE = "http://example.com/js/notices.html" | |
IMPLIED_DIRECTORIES = [".", ".."] | |
attr_reader :options | |
def get_columns | |
if ENV['TERM'].nil? | |
80 | |
else | |
`tput cols`.to_i | |
end | |
end | |
def initialize(options = nil) | |
# Set defaults | |
@options = { | |
:verbose => false, | |
:quiet => false, | |
:short => false, | |
:cache_dir => nil, | |
:clear_cache => false | |
} | |
if not options.nil? | |
@options.merge!(options) | |
end | |
end | |
def command_line(arguments, stdin) | |
opts = OptionParser.new | |
opts.on("-c", "--cache DIR", "Set a directory to cache UglifyJS output") do |dir| | |
@options[:cache_dir] = dir | |
end | |
opts.on("-n", "--no-cache", "Clears the cache before running") do | |
@options[:clear_cache] = true | |
end | |
opts.on("-V", "--verbose", "Verbose output") do | |
@options[:verbose] = true | |
end | |
opts.on("-s", "--short", "Output a more compact status") do | |
@options[:short] = true | |
end | |
opts.on("-q", "--quiet", "Output as little as possible (overrides verbose and short)") do | |
@options[:quiet] = true | |
end | |
opts.on("-v", "--version", "Display the version, then exits") do | |
output_version | |
exit true | |
end | |
opts.on("-h", "--help", "Displays help message") do | |
print File.basename(__FILE__) << " Version " | |
output_version | |
puts "A script to run a group of Javascript files through UglifyJS for a production server." | |
puts | |
puts opts | |
exit true | |
end | |
opts.banner = "Usage: " << File.basename(__FILE__) << " [options] json_config_file output_directory" | |
# process options | |
@options[:verbose] = false if @options[:quiet] | |
@options[:short] = false if @options[:quiet] | |
if (opts.parse!(arguments) rescue false) && arguments.length == 2 | |
opts = nil | |
package(arguments[0], arguments[1]) | |
else | |
puts opts | |
exit false | |
end | |
end | |
def package(config, output_dir) | |
puts "Start at #{DateTime.now}" if @options[:verbose] | |
check_file_path config | |
input_dir = File.dirname config | |
check_dir_path output_dir | |
if not @options[:cache_dir].nil? | |
check_dir_path @options[:cache_dir] | |
end | |
if `which uglifyjs`.empty? | |
abort "Can't locate UglifyJS!" | |
end | |
if @options[:clear_cache] and not @options[:cache_dir].nil? | |
delete_directory(@options[:cache_dir], false) | |
end | |
begin | |
input = File.open(config, "r") { |f| f.read } | |
rescue | |
abort "Error reading #{config}!" | |
end | |
begin | |
input = JSON.parse(input, {:symbolize_names => true}) | |
rescue | |
abort "Error parsing #{config} as JSON!" | |
end | |
ugly = false | |
copyright = [] | |
input.each do |set| | |
if set.class == String | |
# single file | |
set = { :file => set, :sources => [set] } | |
elsif set[:sources].nil? | |
# single file with details | |
set = { :file => set[:file], :sources => [set] } | |
end | |
if set[:file].class != String | |
abort "JSON contains a \"file\" key that is not a String!" | |
end | |
bundle = StringIO.new | |
set_copyright = false | |
first = true | |
set[:sources].each do |item| | |
if item.class == Hash | |
if item.has_key? :home | |
if not copyright.include? item | |
copyright.push item | |
end | |
set_copyright = true | |
end | |
else | |
item = {:file => item} | |
end | |
file = File.join(input_dir, item[:file]) | |
generate = true | |
if File.exists? file | |
bundle.puts "// " << item[:file] if set[:sources].count > 1 | |
raw = File.read(file) | |
md5 = Digest::MD5.hexdigest(raw) | |
if @options[:cache_dir].nil? | |
cache_exists = false | |
else | |
cache_file = File.join(@options[:cache_dir], item[:file]) | |
cache_exists = File.exists?(cache_file) | |
end | |
if cache_exists | |
begin | |
File.open(cache_file, "r") do |cache| | |
cache_md5 = cache.readline[3..-2] | |
if cache_md5 == md5 | |
# md5 matches, read the rest of the cache into the bundle | |
bundle.write cache.read | |
generate = false | |
if not @options[:short] | |
status(item[:file], "Cached", OLIVE) | |
end | |
end | |
end | |
rescue | |
warn "Error while reading \"#{cache_file}\"!" | |
end | |
end | |
if generate | |
if @options[:short] | |
if first | |
first = false | |
else | |
print ", " | |
end | |
end | |
if not ugly | |
require "uglifier" | |
require "active_support/core_ext" | |
ugly = true | |
end | |
begin | |
compressed = Uglifier.new(:copyright => false).compile(raw) << ";" | |
rescue | |
compressed = nil | |
end | |
if not compressed.nil? | |
if not @options[:cache_dir].nil? | |
check_file_path cache_file | |
File.open(cache_file, "w") do |cache| | |
cache.puts "// " << md5 | |
cache.puts compressed | |
end | |
end | |
bundle.puts compressed | |
status(item[:file], "UglifyJS", GREEN) | |
else | |
bundle.puts "// ERROR - UglifyJS had an error!" | |
status(item[:file], "UglifyJS", RED) | |
if @options[:quiet] or @options[:verbose] | |
warn "UglifyJS had an error on file \"#{item[:file]}\"" | |
end | |
end | |
end | |
else | |
bundle.puts "// ERROR - Source file is missing!" | |
status(item[:file], "Missing!", RED) | |
if @options[:quiet] or @options[:verbose] | |
warn "\"#{item[:file]}\" does not exist!" | |
end | |
end | |
end | |
if set_copyright | |
bundle.string.insert(0, "// Copyright information is available at #{COPYRIGHT_FILE}\n") | |
end | |
bundle_path = File.join(output_dir, set[:file]) | |
check_file_path bundle_path | |
skip_writeout = false | |
if File.exists? bundle_path | |
raw = File.read(bundle_path) | |
md5 = Digest::MD5.hexdigest(raw) | |
new_md5 = Digest::MD5.hexdigest(bundle.string) | |
skip_writeout = (md5 == new_md5) | |
end | |
if skip_writeout | |
if not @options[:quiet] | |
if @options[:short] | |
if not first | |
puts " ==> " << set[:file] << " (No change)" | |
end | |
else | |
puts "No changes needed for " << escape(BOLD) << set[:file] << escape(BOLD_RESET) | |
end | |
end | |
else | |
File.open(bundle_path, "w") do |output| | |
output.write bundle.string | |
end | |
if not @options[:quiet] | |
if @options[:short] | |
puts " ==> " << escape(BOLD) << set[:file] << escape(BOLD_RESET) | |
else | |
puts "Bundle written to " << escape(BOLD) << set[:file] << escape(BOLD_RESET) | |
end | |
end | |
end | |
end | |
if copyright.count > 0 | |
copyright_path = File.join(output_dir, COPYRIGHT_FILE) | |
check_file_path copyright_path | |
File.open(copyright_path, "w") do |f| | |
f.puts "<!DOCTYPE html>" | |
title = "Javascript Libraries" | |
f.puts "<html><head><title>#{title}</title></head><body><h1>#{title}</h1>" << | |
"<p>The following Javascript libraries are repackaged into concatenated files. " << | |
"Full copyright notices, licence information and other headers are " << | |
"available in the source files available on their respective websites:</p><ul>" | |
copyright.each do |item| | |
f.puts "<li><a href=\"#{item[:home]}\">#{item[:name]}</a></li>" | |
end | |
f.puts "</ul></body></html>" | |
end | |
if @options[:verbose] | |
puts "Copyright notices written to " << | |
escape(BOLD) << COPYRIGHT_FILE << escape(BOLD_RESET) | |
end | |
end | |
puts "Finished at #{DateTime.now}" if @options[:verbose] | |
end | |
protected | |
def output_version | |
puts VERSION | |
end | |
def escape(code) | |
"\033[#{code}m" | |
end | |
def status(file, status, color = "0") | |
if not @options[:quiet] | |
if @options[:short] | |
print escape(color) << file << escape(RESET) | |
else | |
print file + escape(GREY) | |
columns = get_columns | |
length = file.length % columns + status.length % columns | |
if length < columns | |
print SPACER * (columns - length) | |
else | |
#too long, split onto two lines | |
puts SPACER * (columns - file.length % columns) | |
print SPACER * (columns - status.length % columns) | |
end | |
puts escape(color) << status << escape(RESET) | |
end | |
end | |
end | |
def delete_directory(dir, delete_me = true) | |
Dir.chdir(dir) do | |
Dir.entries(".").each do |f| | |
if f != "." and f != ".." | |
if File.directory? f | |
delete_directory f | |
else | |
File.delete f | |
end | |
end | |
end | |
end | |
Dir.rmdir(dir) if delete_me | |
end | |
def check_dirs(bits) | |
if bits.count > 0 | |
dir = bits.shift | |
if not IMPLIED_DIRECTORIES.include? dir | |
if not File.exists? dir | |
begin | |
Dir.mkdir dir | |
rescue | |
abort "Can't make directory \"#{File.join(Dir.pwd, dir)}\"!" | |
end | |
end | |
if not File.directory? dir | |
abort "\"#{File.join(Dir.pwd, dir)}\" is not a directory!" | |
end | |
end | |
if bits.count > 0 | |
Dir.chdir(dir) do | |
check_dirs(bits) | |
end | |
end | |
end | |
end | |
def check_dir_path(path) | |
if not File.directory? path | |
bits = path.split(File::SEPARATOR) | |
if bits.first == "" | |
bits.shift | |
if bits.include? "" | |
abort "\"#{path}\" is not a correct path!" | |
else | |
Dir.chdir("/") do | |
check_dirs bits | |
end | |
end | |
elsif bits.include? "" | |
abort "\"#{path}\" is not a correct path!" | |
else | |
check_dirs bits | |
end | |
end | |
end | |
def check_file_path(path) | |
if File.directory? path | |
warn "\"#{path}\" is a directory, expected a file!" | |
elsif not File.exists? path | |
check_dir_path(File.dirname(path)) | |
end | |
end | |
end | |
if __FILE__ == $0 | |
app = JSPackage.new() | |
app.command_line(ARGV, STDIN) | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[ | |
"single-file.js", | |
{ | |
"file": "output-file.js", | |
"sources": [ | |
{ | |
"file": "subfolder/library.js", | |
"name": "JS Library name", | |
"home": "http://example.com/js-library" | |
}, | |
"source-file.js", | |
"other-source-file.js" | |
] | |
}, | |
"other-single-file.js" | |
] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment