lib/uglifier.rb in uglifier-1.3.0 vs lib/uglifier.rb in uglifier-2.0.0

- old
+ new

@@ -3,34 +3,55 @@ require "execjs" require "multi_json" class Uglifier Error = ExecJS::Error - # MultiJson.engine = :json_gem # Default options for compilation DEFAULTS = { - :mangle => true, # Mangle variable and function names, use :vars to skip function mangling - :toplevel => false, # Mangle top-level variable names - :except => ["$super"], # Variable names to be excluded from mangling - :max_line_length => 32 * 1024, # Maximum line length - :squeeze => true, # Squeeze code resulting in smaller, but less-readable code - :seqs => true, # Reduce consecutive statements in blocks into single statement - :dead_code => true, # Remove dead code (e.g. after return) - :lift_vars => false, # Lift all var declarations at the start of the scope - :unsafe => false, # Optimizations known to be unsafe in some situations - :copyright => true, # Show copyright message - :ascii_only => false, # Encode non-ASCII characters as Unicode code points - :inline_script => false, # Escape </script - :quote_keys => false, # Quote keys in object literals + :output => { + :ascii_only => false, # Escape non-ASCII characterss + :comments => :copyright, # Preserve comments (:all, :jsdoc, :copyright, :none) + :inline_script => false, # Escape occurrences of </script in strings + :quote_keys => 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 - :beautify => false, # Ouput indented code - :beautify_options => { - :indent_level => 4, - :indent_start => 0, - :space_colon => false - } + :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__) @@ -42,122 +63,196 @@ # 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 = DEFAULTS.merge(options) + (options.keys - DEFAULTS.keys - [:comments, :squeeze])[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) - source = source.respond_to?(:read) ? source.read : source.to_s + really_compile(source, false) + end + alias_method :compress, :compile - js = [] - js << "var result = '';" - js << "var source = #{json_encode(source)};" - js << "var ast = UglifyJS.parser.parse(source);" + # 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 - if @options[:lift_vars] - js << "ast = UglifyJS.uglify.ast_lift_variables(ast);" - end + private - if @options[:copyright] - js << <<-JS - var comments = UglifyJS.parser.tokenizer(source)().comments_before; - for (var i = 0; i < comments.length; i++) { - var c = comments[i]; - result += (c.type == "comment1") ? "//"+c.value+"\\n" : "/*"+c.value+"*/\\n"; + # 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; + } } - JS - end - js << "ast = UglifyJS.uglify.ast_mangle(ast, #{json_encode(mangle_options)});" + var options = %s; + var source = options.source; + var ast = UglifyJS.parse(source, options.parse_options); + ast.figure_out_scope(); - if @options[:squeeze] - js << "ast = UglifyJS.uglify.ast_squeeze(ast, #{json_encode(squeeze_options)});" - end + if (options.compress) { + var compressor = UglifyJS.Compressor(options.compress); + ast = ast.transform(compressor); + ast.figure_out_scope(); + } - if @options[:unsafe] - js << "ast = UglifyJS.uglify.ast_squeeze_more(ast);" - end + if (options.mangle) { + ast.compute_char_frequency(); + ast.mangle_names(options.mangle); + } - js << "result += UglifyJS.uglify.gen_code(ast, #{json_encode(gen_code_options)});" + var gen_code_options = options.output; + gen_code_options.comments = comments(options.output.comments); - if !@options[:beautify] && @options[:max_line_length] - js << "result = UglifyJS.uglify.split_lines(result, #{@options[:max_line_length].to_i})" - end + if (options.generate_map) { + var source_map = UglifyJS.SourceMap(options.source_map_options); + gen_code_options.source_map = source_map; + } - js << "return result + ';';" + var stream = UglifyJS.OutputStream(gen_code_options); - @context.exec js.join("\n") + 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 - alias_method :compress, :compile - private - def mangle_options - { - "mangle" => @options[:mangle], - "toplevel" => @options[:toplevel], - "defines" => defines, - "except" => @options[:except], - "no_functions" => @options[:mangle] == :vars - } + conditional_option(@options[:mangle], DEFAULTS[:mangle]) end - def squeeze_options - { - "make_seqs" => @options[:seqs], - "dead_code" => @options[:dead_code], - "keep_comps" => !@options[:unsafe] - } + def compressor_options + defaults = conditional_option(DEFAULTS[:compress], + :global_defs => @options[:define] || {}) + conditional_option(@options[:compress] || @options[:squeeze], defaults) end - def defines - Hash[(@options[:define] || {}).map do |k, v| - token = if v.is_a? Numeric - ['num', v] - elsif [true, false].include?(v) - ['name', v.to_s] - elsif v == nil - ['name', 'null'] - else - ['string', v.to_s] - end - [k, token] - 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] + 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 gen_code_options - options = { - :ascii_only => @options[:ascii_only], - :inline_script => @options[:inline_script], - :quote_keys => @options[:quote_keys] + 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 - if @options[:beautify] - options.merge(:beautify => true).merge(@options[:beautify_options]) - else - options - 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