# encoding: UTF-8 require "execjs" require "multi_json" class Uglifier Error = ExecJS::Error # Default options for compilation DEFAULTS = { :output => { :ascii_only => false, # Escape non-ASCII characterss :comments => :copyright, # Preserve comments (:all, :jsdoc, :copyright, :none) :inline_script => false, # Escape occurrences of false, # Quote keys in object literals :max_line_len => 32 * 1024, # Maximum line length in minified code :ie_proof => true, # Output block brakcets around do-while loops :bracketize => false, # Bracketize if, for, do, while or with statements, even if their body is a single statement :semicolons => true, # Separate statements with semicolons :preserve_line => false, # Preserve line numbers in outputs :beautify => false, # Beautify output :indent_level => 4, # Indent level in spaces :indent_start => 0, # Starting indent level :space_colon => false, # Insert space before colons (only with beautifier) :width => 80 # Specify line width when beautifier is used (only with beautifier) }, :mangle => { :except => ["$super"] # Argument names to be excluded from mangling }, # Mangle variable and function names, set to false to skip mangling :compress => { :sequences => true, # Allow statements to be joined by commas :properties => true, # Rewrite property access using the dot notation :dead_code => true, # Remove unreachable code :drop_debugger => true, # Remove debugger; statements :unsafe => false, # Apply "unsafe" transformations :conditionals => true, # Optimize for if-s and conditional expressions :comparisons => true, # Apply binary node optimizations for comparisons :evaluate => true, # Attempt to evaluate constant expressions :booleans => true, # Various optimizations to boolean contexts :loops => true, # Optimize lops when condition can be statically determined :unused => true, # Drop unreferenced functions and variables :hoist_funs => true, # Hoist function declarations :hoist_vars => false, # Hoist var declarations :if_return => true, # Optimizations for if/return and if/continue :join_vars => true, # Join consecutive var statements :cascade => true # Cascade sequences }, # Apply transformations to code, set to false to skip :define => {}, # Define values for symbol replacement :source_filename => nil, # The filename of the input file :source_root => nil, # The URL of the directory which contains :source_filename :output_filename => nil, # The filename or URL where the minified output can be found :input_source_map => nil # The contents of the source map describing the input } SourcePath = File.expand_path("../uglify.js", __FILE__) ES5FallbackPath = File.expand_path("../es5.js", __FILE__) # Minifies JavaScript code using implicit context. # # source should be a String or IO object containing valid JavaScript. # options contain optional overrides to Uglifier::DEFAULTS # # Returns minified code as String def self.compile(source, options = {}) self.new(options).compile(source) end # Minifies JavaScript code and generates a source map using implicit context. # # source should be a String or IO object containing valid JavaScript. # options contain optional overrides to Uglifier::DEFAULTS # # Returns a pair of [minified code as String, source map as a String] def self.compile_with_map(source, options = {}) self.new(options).compile_with_map(source) end # Initialize new context for Uglifier with given options # # options - Hash of options to override Uglifier::DEFAULTS def initialize(options = {}) (options.keys - DEFAULTS.keys - [:comments, :squeeze, :copyright])[0..1].each do |missing| raise ArgumentError.new("Invalid option: #{missing}") end @options = options @context = ExecJS.compile(File.open(ES5FallbackPath, "r:UTF-8").read + File.open(SourcePath, "r:UTF-8").read) end # Minifies JavaScript code # # source should be a String or IO object containing valid JavaScript. # # Returns minified code as String def compile(source) really_compile(source, false) end alias_method :compress, :compile # Minifies JavaScript code and generates a source map # # source should be a String or IO object containing valid JavaScript. # # Returns a pair of [minified code as String, source map as a String] def compile_with_map(source) really_compile(source, true) end private # Minifies JavaScript code # # source should be a String or IO object containing valid JavaScript. def really_compile(source, generate_map) source = source.respond_to?(:read) ? source.read : source.to_s js = <<-JS function comments(option) { if (Object.prototype.toString.call(option) === '[object Array]') { return new RegExp(option[0], option[1]); } else if (option == "jsdoc") { return function(node, comment) { if (comment.type == "comment2") { return /@preserve|@license|@cc_on/i.test(comment.value); } else { return false; } } } else { return option; } } var options = %s; var source = options.source; var ast = UglifyJS.parse(source, options.parse_options); ast.figure_out_scope(); if (options.compress) { var compressor = UglifyJS.Compressor(options.compress); ast = ast.transform(compressor); ast.figure_out_scope(); } if (options.mangle) { ast.compute_char_frequency(); ast.mangle_names(options.mangle); } var gen_code_options = options.output; gen_code_options.comments = comments(options.output.comments); if (options.generate_map) { var source_map = UglifyJS.SourceMap(options.source_map_options); gen_code_options.source_map = source_map; } var stream = UglifyJS.OutputStream(gen_code_options); ast.print(stream); if (options.generate_map) { return [stream.toString(), source_map.toString()]; } else { return stream.toString(); } JS @context.exec(js % json_encode( :source => source, :output => output_options, :compress => compressor_options, :mangle => mangle_options, :parse_options => parse_options, :source_map_options => source_map_options, :generate_map => (!!generate_map) )) end def mangle_options conditional_option(@options[:mangle], DEFAULTS[:mangle]) end def compressor_options defaults = conditional_option(DEFAULTS[:compress], :global_defs => @options[:define] || {}) conditional_option(@options[:compress] || @options[:squeeze], defaults) end def comment_options val = if @options.has_key?(:output) && @options[:output].has_key?(:comments) @options[:output][:comments] elsif @options.has_key?(:comments) @options[:comments] elsif @options[:copyright] == false :none else DEFAULTS[:output][:comments] end case val when :all, true true when :jsdoc "jsdoc" when :copyright encode_regexp(/Copyright/i) when Regexp encode_regexp(val) else false end end def output_options DEFAULTS[:output].merge(@options[:output] || {}).merge( :comments => comment_options) end def source_map_options { :file => @options[:output_filename], :root => @options[:source_root], :orig => @options[:input_source_map] } end def parse_options {:filename => @options[:source_filename]} end # MultiJson API detection if MultiJson.respond_to? :dump def json_encode(obj) MultiJson.dump(obj) end else def json_encode(obj) MultiJson.encode(obj) end end def encode_regexp(regexp) modifiers = if regexp.casefold? "i" else "" end [regexp.source, modifiers] end def conditional_option(value, defaults) if value == true || value == nil defaults elsif value defaults.merge(value) else false end end end