lib/uglifier.rb in uglifier-2.7.2 vs lib/uglifier.rb in uglifier-3.0.0

- old
+ new

@@ -1,87 +1,25 @@ # encoding: UTF-8 -require "execjs" require "json" +require "base64" +require "execjs" require "uglifier/version" # A wrapper around the UglifyJS interface class Uglifier # Error class for compilation errors. Error = ExecJS::Error - # JavaScript code to call UglifyJS - JS = <<-JS - (function(options) { - 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 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); - } - - if (options.enclose) { - ast = ast.wrap_enclose(options.enclose); - } - - 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.source_map_options.map_url) { - stream += "\\n//# sourceMappingURL=" + options.source_map_options.map_url; - } - - if (options.source_map_options.url) { - stream += "\\n//# sourceURL=" + options.source_map_options.url; - } - - if (options.generate_map) { - return [stream.toString(), source_map.toString()]; - } else { - return stream.toString(); - } - }) - JS - # UglifyJS source path SourcePath = File.expand_path("../uglify.js", __FILE__) # ES5 shims source path ES5FallbackPath = File.expand_path("../es5.js", __FILE__) # String.split shim source path SplitFallbackPath = File.expand_path("../split.js", __FILE__) + # UglifyJS wrapper path + UglifyJSWrapperPath = File.expand_path("../uglifier.js", __FILE__) # Default options for compilation DEFAULTS = { # rubocop:disable LineLength :output => { @@ -102,12 +40,14 @@ }, :mangle => { :eval => false, # Mangle names when eval of when is used in scope :except => ["$super"], # Argument names to be excluded from mangling :sort => false, # Assign shorter names to most frequently used variables. Often results in bigger output after gzip. - :toplevel => false # Mangle names declared in the toplevel scope + :toplevel => false, # Mangle names declared in the toplevel scope + :properties => false # Mangle property names }, # Mangle variable and function names, set to false to skip mangling + :mangle_properties => false, # Mangle property names :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 @@ -121,27 +61,41 @@ :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 + :collapse_vars => false, # Collapse single-use var and const definitions when possible. :negate_iife => true, # Negate immediately invoked function expressions to avoid extra parens :pure_getters => false, # Assume that object property access does not have any side-effects :pure_funcs => nil, # List of functions without side-effects. Can safely discard function calls when the result value is not used :drop_console => false, # Drop calls to console.* functions :angular => false, # Process @ngInject annotations - :keep_fargs => false # Preserve unused function arguments + :keep_fargs => false, # Preserve unused function arguments + :keep_fnames => false # Preserve function names }, # Apply transformations to code, set to false to skip :define => {}, # Define values for symbol replacement :enclose => false, # Enclose in output function wrapper, define replacements as key-value pairs - :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 :screw_ie8 => false, # Don't bother to generate safe code for IE8 - :source_map_url => false, # Url for source mapping to be appended in minified source - :source_url => false # Url for original source to be appended in minified source + :source_map => false # Generate source map } + + LEGACY_OPTIONS = [:comments, :squeeze, :copyright, :mangle] + + MANGLE_PROPERTIES_DEFAULTS = { + :regex => nil # A regular expression to filter property names to be mangled + } + + SOURCE_MAP_DEFAULTS = { + :map_url => false, # Url for source mapping to be appended in minified source + :url => false, # Url for original source to be appended in minified source + :sources_content => false, # Include original source content in map + :filename => nil, # The filename of the input file + :root => nil, # The URL of the directory which contains :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 + } + # rubocop:enable LineLength # Minifies JavaScript code using implicit context. # # @param source [IO, String] valid JS source code. @@ -162,11 +116,11 @@ # Initialize new context for Uglifier with given options # # @param options [Hash] optional overrides to +Uglifier::DEFAULTS+ def initialize(options = {}) - (options.keys - DEFAULTS.keys - [:comments, :squeeze, :copyright])[0..1].each do |missing| + (options.keys - DEFAULTS.keys - LEGACY_OPTIONS)[0..1].each do |missing| raise ArgumentError, "Invalid option: #{missing}" end @options = options @context = ExecJS.compile(uglifyjs_source) end @@ -174,11 +128,18 @@ # Minifies JavaScript code # # @param source [IO, String] valid JS source code. # @return [String] minified code. def compile(source) - run_uglifyjs(source, false) + if @options[:source_map] + compiled, source_map = run_uglifyjs(source, true) + source_map_uri = Base64.strict_encode64(source_map) + source_map_mime = "application/json;charset=utf-8;base64" + compiled + "\n//# sourceMappingURL=data:#{source_map_mime},#{source_map_uri}" + else + run_uglifyjs(source, false) + end end alias_method :compress, :compile # Minifies JavaScript code and generates a source map # @@ -189,29 +150,32 @@ end private def uglifyjs_source - [ES5FallbackPath, SplitFallbackPath, SourcePath].map do |file| - File.open(file, "r:UTF-8") { |f| f.read } + [ES5FallbackPath, SplitFallbackPath, SourcePath, UglifyJSWrapperPath].map do |file| + File.open(file, "r:UTF-8", &:read) end.join("\n") end # Run UglifyJS for given source code - def run_uglifyjs(source, generate_map) + def run_uglifyjs(input, generate_map) + source = read_source(input) + input_map = input_source_map(source, generate_map) options = { - :source => read_source(source), + :source => source, :output => output_options, :compress => compressor_options, :mangle => mangle_options, + :mangle_properties => mangle_properties_options, :parse_options => parse_options, - :source_map_options => source_map_options, + :source_map_options => source_map_options(input_map), :generate_map => generate_map, :enclose => enclose_options } - @context.call(Uglifier::JS, options) + @context.call("uglifier", options) end def read_source(source) if source.respond_to?(:read) source.read @@ -219,18 +183,29 @@ source.to_s end end def mangle_options - conditional_option(@options[:mangle], DEFAULTS[:mangle]) + mangle_options = @options.fetch(:mangle, @options[:mangle]) + conditional_option(mangle_options, DEFAULTS[:mangle]) end + def mangle_properties_options + mangle_options = @options.fetch(:mangle_properties, DEFAULTS[:mangle_properties]) + options = conditional_option(mangle_options, MANGLE_PROPERTIES_DEFAULTS) + if options && options[:regex] + options.merge(:regex => encode_regexp(options[:regex])) + else + options + end + end + def compressor_options defaults = conditional_option( DEFAULTS[:compress], :global_defs => @options[:define] || {}, - :screw_ie8 => @options[:screw_ie8] || DEFAULTS[:screw_ie8] + :screw_ie8 => screw_ie8? ) conditional_option(@options[:compress] || @options[:squeeze], defaults) end def comment_options @@ -267,28 +242,35 @@ ).reject { |key, _| key == :ie_proof } end def screw_ie8? if (@options[:output] || {}).has_key?(:ie_proof) - false + !@options[:output][:ie_proof] else - @options[:screw_ie8] || DEFAULTS[:screw_ie8] + @options.fetch(:screw_ie8, DEFAULTS[:screw_ie8]) end end - def source_map_options + def source_map_options(input_map) + options = conditional_option(@options[:source_map], SOURCE_MAP_DEFAULTS) + { - :file => @options[:output_filename], - :root => @options[:source_root], - :orig => @options[:input_source_map], - :map_url => @options[:source_map_url], - :url => @options[:source_url] + :file => options[:output_filename], + :root => options.fetch(:root) { input_map ? input_map["sourceRoot"] : nil }, + :orig => input_map, + :map_url => options[:map_url], + :url => options[:url], + :sources_content => options[:sources_content] } end def parse_options - { :filename => @options[:source_filename] } + if @options[:source_map].respond_to?(:[]) + { :filename => @options[:source_map][:filename] } + else + {} + end end def enclose_options if @options[:enclose] @options[:enclose].map do |pair| @@ -315,7 +297,41 @@ elsif value defaults.merge(value) else false end + end + + def sanitize_map_root(map) + if map.nil? + nil + elsif map.is_a? String + sanitize_map_root(JSON.load(map)) + elsif map["sourceRoot"] == "" + map.merge("sourceRoot" => nil) + else + map + end + end + + def extract_source_mapping_url(source) + comment_start = %r{(?://|/\*\s*)} + comment_end = %r{\s*(?:\r?\n?\*/|$)?} + source_mapping_regex = /#{comment_start}[@#]\ssourceMappingURL=\s*(\S*?)#{comment_end}/ + rest = /\s#{comment_start}[@#]\s[a-zA-Z]+=\s*(?:\S*?)#{comment_end}/ + regex = /#{source_mapping_regex}(?:#{rest})*\Z/m + match = regex.match(source) + match && match[1] + end + + def input_source_map(source, generate_map) + return nil unless generate_map + sanitize_map_root(@options.fetch(:source_map, {}).fetch(:input_source_map) do + url = extract_source_mapping_url(source) + if url && url.start_with?("data:") + Base64.strict_decode64(url.split(",", 2)[-1]) + end + end) + rescue ArgumentError, JSON::ParserError + nil end end