Its very likely that a web application serves the Javascript and CSS in a minified/uglified format. This is done usually to reduce the file size to be downloaded by the clients and thus improving the load time for the applications.

Uglifiers rename function and variable names to single letters and removes newline characters. This makes it very hard to read the scripts for developers via browser console or by other means.

Uglifying and debugging

One could look in a browser’s Javascript console to see and understand any Javascript errors that occur on a page. However, when the Javascript is minified, one would see an error but can understand very little from it.

For example, see the following error and its stack trace in the console.

TypeError: Unable to get property 'id' of undefined
  at Anonymous function(/assets/onboarding/index-772a8127b092d6eaf7bf19b399517afdc847925d9281d3f0b35681eb6741817f.js:1:15624)
  at lt.event.dispatch(/assets/jquery-c567de05321bae803d07181b60c2cedba30ec424166235b5075d83a91ff72233.js:2:6550)
  ...

One can tell which compiled Javascript file has the error but will not be able to tell where exactly in the file and which function or variable is causing the error.

This is where source maps come in handy.

What are source maps?

Simple put, source map is a file that contains information on how a Javascript or a CSS file is uglified. This will contain the mapping of actual to shortened variable names, different scripts that are combined to form a single minified script and the actual source itself or a URL to access the original source.

From the above example, if there is a source map, the stack trace would look like this:

TypeError: Cannot read property 'id' of undefined
  at apply(/assets/onboarding/index.js:660:36)
  at apply(/assets/jquery.js:3075:9)

Source map contents are put in a different file and the uglified file refers the source map file using a special comment:

...
//# sourceMappingURL=https://my-domain.com/path/to/file.js.map

Sprockets and Rails

Up until Rails version 5.*, the framework implemented asset pipeline using Sprockets. Sprockets is a Ruby library for compiling and serving web assets. It allows to combine multiple files into one and uglify the resulting files.

Sprockets is shipped with different Uglifiers (or compressors) and Uglifier is one of them. Its configured in the environment’s config file in Rails using the following line.

  config.assets.js_compressor = :uglifier

Sprockets and Source maps

Sprockets version 4 supports generation of source maps. However, version 4 is still in beta. If one is on version 3, source maps generation with uglifier JS compressor is not supported. However, Uglifier wrapper itself supports generation of source maps.

Generation of source maps

Sprockets support writing your own JS compressor and registering it with sprockets to use it. Since we know that Uglifier already supports generation of source maps, we will extend the Sprockets::UglifierCompressor from Sprockets to support source maps generation.

To do so, we create an initializer in our Rails application (config/initializers/sprockets_uglifier_with_source_maps_compressor.rb) with the following contents.

require 'sprockets/digest_utils'
require 'sprockets/uglifier_compressor'

module Sprockets
  class UglifierWithSourceMapsCompressor < Sprockets::UglifierCompressor
    def call(input)
      data = input.fetch(:data) # Non-Uglified contents of the JS file
      name = input.fetch(:name) # name of the to-be-compressed JS file.

      @uglifier ||= Autoload::Uglifier.new(@options)
      compressed_data, sourcemap_json = @uglifier.compile_with_map(input[:data])

      # Update source map according to the version 3 spec: https://sourcemaps.info/spec.html
      sourcemap                   = JSON.parse(sourcemap_json)
      sourcemap['sources']        = [name + '.js']
      sourcemap['sourceRoot']     = ::Rails.application.config.assets.prefix
      sourcemap['sourcesContent'] = [data]
      sourcemap_json              = sourcemap.to_json

      sourcemap_filename = File.join(
        ::Rails.application.config.assets.prefix,
        "#{name}-#{digest(sourcemap_json)}.js.map"
      )
      sourcemap_path = File.join(::Rails.public_path, sourcemap_filename)
      sourcemap_url  = filename_to_url(sourcemap_filename)

      FileUtils.mkdir_p File.dirname(sourcemap_path)
      File.open(sourcemap_path, 'w') { |f| f.write sourcemap_json }

      # Add the source map URL to the compressed JS file.
      compressed_data.concat "\n//# sourceMappingURL=#{sourcemap_url}\n"
    end

    def filename_to_url(filename)
      url_root = ::Rails.application.config.assets.source_maps_domain
      File.join url_root.to_s, filename
    end

    def digest(io)
      Sprockets::DigestUtils.pack_hexdigest Sprockets::DigestUtils.digest(io)
    end
  end
end

Sprockets.register_compressor(
  'application/javascript',
  :uglify_with_source_maps,
  Sprockets::UglifierWithSourceMapsCompressor
)

Lets look at what is happening in the call function:

  • We initialize Uglifier with default options.
  • Compile along with source maps generation.
  • Extend source maps’ JSON to add more information.
    • sources: Specify the filename of the contents’ source.
    • sourcesContent: Original and non-uglified source of the file.
  • Write the file to disk.
  • Append source maps URL to the uglified content.
  • Return the uglified content to Sprockets for it to write it to the asset file.

Now, configure Sprockets to use the new compressor. Add the following line to the respective environment config file (e.g., config/environment/production.rb).

  config.assets.js_compressor = :uglify_with_source_maps

Now, force pre-compilation of the assets and restart Rails server.

# Clear already compiled assets.
rake assets:clobber.
# Precompile assets.
rake assets:precompile.

Visit the application in the browser and navigate to the sources section in the developer section to see the source maps.