Skip to content

Instantly share code, notes, and snippets.

@vdh
Created February 17, 2012 02:02
Show Gist options
  • Save vdh/1849803 to your computer and use it in GitHub Desktop.
Save vdh/1849803 to your computer and use it in GitHub Desktop.
A fairly simple JS packaging script using UglifyJS
#!/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
[
"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