Javascript sourcemaps in Rails with Sprockets 3 and Uglifier.
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.