class Terser

A wrapper around the Terser interface

Constants

DEFAULTS

Default options for compilation

ES5FallbackPath

ES5 shims source path

EXTRA_OPTIONS
MANGLE_PROPERTIES_DEFAULTS
SOURCE_MAP_DEFAULTS
SourceMapPath

Source Map path

SourcePath

TerserJS source path

SplitFallbackPath

String.split shim source path

TerserJSWrapperPath

TerserJS wrapper path

VERSION

Current version of Terser.

Public Class Methods

compile(source, options = {}) click to toggle source

Minifies JavaScript code using implicit context.

@param source [IO, String] valid JS source code. @param options [Hash] optional overrides to Terser::DEFAULTS @return [String] minified code.

# File lib/terser.rb, line 130
def self.compile(source, options = {})
  new(options).compile(source)
end
compile_with_map(source, options = {}) click to toggle source

Minifies JavaScript code and generates a source map using implicit context.

@param source [IO, String] valid JS source code. @param options [Hash] optional overrides to Terser::DEFAULTS @return [Array(String, String)] minified code and source map.

# File lib/terser.rb, line 139
def self.compile_with_map(source, options = {})
  new(options).compile_with_map(source)
end
new(options = {}) click to toggle source

Initialize new context for Terser with given options

@param options [Hash] optional overrides to Terser::DEFAULTS

# File lib/terser.rb, line 146
def initialize(options = {})
  missing = options.keys - DEFAULTS.keys - EXTRA_OPTIONS
  raise ArgumentError, "Invalid option: #{missing.first}" if missing.any?

  @options = options
end

Public Instance Methods

compile(source, source_map_options = @options) click to toggle source

Minifies JavaScript code

@param source [IO, String] valid JS source code. @param source_map_options [Hash] optional @return [String] minified code.

# File lib/terser.rb, line 158
def compile(source, source_map_options = @options)
  if source_map_options[:source_map]
    compiled, source_map = run_terserjs(source, true, source_map_options)
    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_terserjs(source, false)
  end
end
Also aliased as: compress
compile_with_map(source, source_map_options = @options) click to toggle source

Minifies JavaScript code and generates a source map

@param source [IO, String] valid JS source code. @param source_map_options [Hash] optional @return [Array(String, String)] minified code and source map.

# File lib/terser.rb, line 175
def compile_with_map(source, source_map_options = @options)
  run_terserjs(source, true, source_map_options)
end
compress(source, source_map_options = @options)
Alias for: compile

Private Instance Methods

comment_options() click to toggle source
# File lib/terser.rb, line 351
def comment_options
  case comment_setting
  when :all, true
    true
  when :jsdoc
    "jsdoc"
  when :copyright
    encode_regexp(/(^!)|Copyright/i)
  when Regexp
    encode_regexp(comment_setting)
  else
    false
  end
end
comment_setting() click to toggle source
# File lib/terser.rb, line 382
def comment_setting
  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
end
compressor_options() click to toggle source
# File lib/terser.rb, line 329
def compressor_options
  defaults = conditional_option(
    DEFAULTS[:compress],
    :global_defs => @options[:define] || {}
  )

  conditional_option(
    @options[:compress],
    defaults,
    { :keep_fnames => keep_fnames?(:compress) }.merge(negate_iife_block)
  )
end
conditional_option(value, defaults, overrides = {}) click to toggle source
# File lib/terser.rb, line 453
def conditional_option(value, defaults, overrides = {})
  if value == true || value.nil?
    defaults.merge(overrides)
  elsif value
    defaults.merge(value).merge(overrides)
  else
    false
  end
end
context() click to toggle source
# File lib/terser.rb, line 181
def context
  @context ||= begin
    source = source_with(SourcePath)
    ExecJS.compile(source)
  end
rescue ExecJS::RuntimeError, ExecJS::ProgramError => e
  cause = e.cause.to_s
  if cause.include?("missing ; before statement") || cause.include?("Unexpected reserved word")
    raise Error, "Please use a runtime compatible with ECMA6 syntax, the current runtime is #{ExecJS::Runtimes.autodetect.name}"
  end

  raise e
end
context_lines_message(source, line_number, column) click to toggle source
# File lib/terser.rb, line 259
def context_lines_message(source, line_number, column)
  return if line_number.nil?

  line_index = line_number - 1
  lines = source.split("\n")

  first_line = [line_index - error_context_lines, 0].max
  last_line = [line_number + error_context_lines, lines.size].min
  options = error_context_format_options(first_line, last_line, line_index, column)
  context_lines = lines[first_line...last_line]

  "--\n#{format_lines(context_lines, options).join("\n")}\n=="
end
enclose_options() click to toggle source
# File lib/terser.rb, line 433
def enclose_options
  if @options[:enclose]
    @options[:enclose].map do |pair|
      "#{pair.first}:#{pair.last}"
    end
  else
    false
  end
end
encode_regexp(regexp) click to toggle source
# File lib/terser.rb, line 443
def encode_regexp(regexp)
  modifiers = if regexp.casefold?
                "i"
              else
                ""
              end

  [regexp.source, modifiers]
end
error_context_format_options(low, high, line_index, column) click to toggle source
# File lib/terser.rb, line 231
def error_context_format_options(low, high, line_index, column)
  line_width = high.to_s.size
  {
    :line_index => line_index,
    :base_index => low,
    :line_width => line_width,
    :line_format => "\e[36m%#{line_width + 1}d\e[0m ", # cyan
    :col => column
  }
end
error_context_lines() click to toggle source
# File lib/terser.rb, line 227
def error_context_lines
  @options.fetch(:error_context_lines, DEFAULTS[:error_context_lines]).to_i
end
error_message(result, options) click to toggle source
# File lib/terser.rb, line 273
def error_message(result, options)
  err = result['error']
  src_ctx = context_lines_message(options[:source], err['line'], err['col'])
  "#{err['message']}\n#{src_ctx}"
end
extract_source_mapping_url(source) click to toggle source
# File lib/terser.rb, line 475
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
format_error_line(line, options) click to toggle source
# File lib/terser.rb, line 242
def format_error_line(line, options)
  # light red
  indicator = ' => '.rjust(options[:line_width] + 2)
  colored_line = "#{line[0...options[:col]]}\e[91m#{line[options[:col]..-1]}"
  "\e[91m#{indicator}\e[0m#{colored_line}\e[0m"
end
format_lines(lines, options) click to toggle source
# File lib/terser.rb, line 249
def format_lines(lines, options)
  lines.map.with_index do |line, index|
    if options[:base_index] + index == options[:line_index]
      format_error_line(line, options)
    else
      "#{options[:line_format] % (options[:base_index] + index + 1)}#{line}"
    end
  end
end
input_source_map(source, generate_map, options) click to toggle source
# File lib/terser.rb, line 485
def input_source_map(source, generate_map, options)
  return nil unless generate_map

  source_map_options = options[:source_map].is_a?(Hash) ? options[:source_map] : {}
  sanitize_map_root(source_map_options.fetch(:input_source_map) do
    url = extract_source_mapping_url(source)
    Base64.strict_decode64(url.split(",", 2)[-1]) if url && url.start_with?("data:")
  end)
rescue ArgumentError, JSON::ParserError
  nil
end
keep_fnames?(type) click to toggle source
# File lib/terser.rb, line 397
def keep_fnames?(type)
  if @options[:keep_fnames] || DEFAULTS[:keep_fnames]
    true
  else
    @options[type].respond_to?(:[]) && @options[type][:keep_fnames] ||
      DEFAULTS[type].respond_to?(:[]) && DEFAULTS[type][:keep_fnames]
  end
end
mangle_options() click to toggle source
# File lib/terser.rb, line 297
def mangle_options
  defaults = conditional_option(
    DEFAULTS[:mangle],
    :keep_fnames => keep_fnames?(:mangle)
  )

  conditional_option(
    @options[:mangle],
    defaults,
    :properties => mangle_properties_options
  )
end
mangle_properties_options() click to toggle source
# File lib/terser.rb, line 310
def mangle_properties_options
  mangle_options = conditional_option(@options[:mangle], DEFAULTS[:mangle])

  mangle_properties_options =
    if @options.has_key?(:mangle_properties)
      @options[:mangle_properties]
    else
      mangle_options && mangle_options[:properties]
    end

  options = conditional_option(mangle_properties_options, MANGLE_PROPERTIES_DEFAULTS)

  if options && options[:regex]
    options.merge(:regex => encode_regexp(options[:regex]))
  else
    options
  end
end
negate_iife_block() click to toggle source

Prevent negate_iife when wrap_iife is true

# File lib/terser.rb, line 343
def negate_iife_block
  if output_options[:wrap_iife]
    { :negate_iife => false }
  else
    {}
  end
end
output_options() click to toggle source
# File lib/terser.rb, line 392
def output_options
  DEFAULTS[:output].merge(@options[:output] || {})
                   .merge(:comments => comment_options, :quote_style => quote_style)
end
parse_options(source_map_options) click to toggle source
# File lib/terser.rb, line 420
def parse_options(source_map_options)
  conditional_option(@options[:parse], DEFAULTS[:parse])
    .merge(parse_source_map_options(source_map_options))
end
parse_result(result, generate_map, options, source_map_options = {}) click to toggle source
# File lib/terser.rb, line 279
def parse_result(result, generate_map, options, source_map_options = {})
  raise Error, error_message(result, options) if result.has_key?('error')

  if generate_map
    [result['code'] + source_map_comments(source_map_options), result['map']]
  else
    result['code'] + source_map_comments(source_map_options)
  end
end
parse_source_map_options(source_map_options) click to toggle source
# File lib/terser.rb, line 425
def parse_source_map_options(source_map_options)
  if source_map_options[:source_map].respond_to?(:[])
    { :filename => source_map_options[:source_map][:filename] }
  else
    {}
  end
end
quote_style() click to toggle source
# File lib/terser.rb, line 366
def quote_style
  option = conditional_option(@options[:output], DEFAULTS[:output])[:quote_style]
  case option
  when :single
    1
  when :double
    2
  when :original
    3
  when Numeric
    option
  else # auto
    0
  end
end
read_source(source) click to toggle source
# File lib/terser.rb, line 289
def read_source(source)
  if source.respond_to?(:read)
    source.read
  else
    source.to_s
  end
end
run_terserjs(input, generate_map, source_map_options = {}) click to toggle source

Run TerserJS for given source code

# File lib/terser.rb, line 212
def run_terserjs(input, generate_map, source_map_options = {})
  source = read_source(input)
  input_map = input_source_map(source, generate_map, source_map_options)
  options = {
    :source => source,
    :output => output_options,
    :compress => compressor_options,
    :mangle => mangle_options,
    :parse => parse_options(source_map_options),
    :sourceMap => source_map_options(input_map, source_map_options)
  }

  parse_result(context.call("terser_wrapper", options), generate_map, options, source_map_options)
end
sanitize_map_root(map) click to toggle source
# File lib/terser.rb, line 463
def sanitize_map_root(map)
  if map.nil?
    nil
  elsif map.is_a? String
    sanitize_map_root(JSON.parse(map))
  elsif map["sourceRoot"] == ""
    map.merge("sourceRoot" => nil)
  else
    map
  end
end
source_map_comments(source_map_options) click to toggle source
# File lib/terser.rb, line 195
def source_map_comments(source_map_options)
  return '' unless source_map_options[:source_map].respond_to?(:[])

  suffix = ''
  suffix += "\n//# sourceMappingURL=#{source_map_options[:source_map][:map_url]}" if source_map_options[:source_map][:map_url]
  suffix += "\n//# sourceURL=#{source_map_options[:source_map][:url]}" if source_map_options[:source_map][:url]
  suffix
end
source_map_options(input_map, source_map_options) click to toggle source
# File lib/terser.rb, line 406
def source_map_options(input_map, source_map_options)
  options = conditional_option(source_map_options[:source_map], SOURCE_MAP_DEFAULTS) || SOURCE_MAP_DEFAULTS

  {
    :input => options[:filename],
    :filename => options[:output_filename],
    :root => options.fetch(:root) { input_map ? input_map["sourceRoot"] : nil },
    :content => input_map,
    #:map_url => options[:map_url],
    :url => options[:url],
    :includeSources => options[:sources_content]
  }
end
source_with(path) click to toggle source
# File lib/terser.rb, line 204
def source_with(path)
  [ES5FallbackPath, SplitFallbackPath, SourceMapPath, path,
   TerserJSWrapperPath].map do |file|
    File.open(file, "r:UTF-8", &:read)
  end.join("\n")
end