Skip to content

Commit

Permalink
Enable sourcemaps by default.
Browse files Browse the repository at this point in the history
This also adds support for customizing sourcemap links on the command
line and via the Ruby option API.

Closes #1189
  • Loading branch information
nex3 committed Jun 27, 2014
1 parent 3bd7fc0 commit 4da1c1a
Show file tree
Hide file tree
Showing 14 changed files with 178 additions and 117 deletions.
10 changes: 10 additions & 0 deletions doc-src/SASS_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ adding multiple suffixes to the same parent selector. For example:

### Smaller Improvements

* Sourcemaps are now generated by default when using the `sass` or `scss`
executables and when using Sass as a plugin. This can be disabled by passing
`--sourcemap=none` on the command line or setting the `:sourcemap` option to
`:none` in Ruby.

* If a relative URI from a sourcemap to a Sass file can't be generated, it will
now fall back to using an absolute "file:" URI. In addition,
`--sourcemap=file` can be passed on the command line to force all URIs to Sass
files to be absolute.

* `@extend` can now extend selectors within selector pseudoclasses such as
`:not` and `:matches`. This also means that placeholder selectors can be used
within selector pseudoclasses. This behavior can be detected using
Expand Down
17 changes: 9 additions & 8 deletions doc-src/SASS_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,14 +294,15 @@ Available options are:
Defaults to {Sass::Importers::Filesystem}.

{#sourcemap-option} `:sourcemap`
: When set to true, causes Sass to generate standard JSON [source maps][]
alongside its compiled CSS files. These source maps tell the browser how to
find the Sass styles that caused each CSS style to be generated. Sass assumes
that the source stylesheets will be made available on whatever server you're
using, and that their relative location will be the same as it is on the local
filesystem. If this isn't the case, you'll need to make a custom class that
extends \{Sass::Importers::Base} or \{Sass::Importers::Filesystem} and
overrides \{Sass::Importers::Base#public\_url `#public_url`}.
: Controls how sourcemaps are generated. These sourcemaps tell the browser how
to find the Sass styles that caused each CSS style to be generated. This has
three valid values: **`:auto`** uses relative URIs where possible, assuming
that that the source stylesheets will be made available on whatever server
you're using, and that their relative location will be the same as it is on
the local filesystem. If a relative URI is unavailable, a "file:" URI is used
instead. **`:file`** always uses "file:" URIs, which will work locally but
can't be deployed to a remote server. Finally, **`:none`** causes no
sourcemaps to be generated at all.

{#line_numbers-option} `:line_numbers`
: When set to true, causes the line number and file
Expand Down
6 changes: 5 additions & 1 deletion lib/sass/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ def self.normalize_options(options)
when :alternate; options[:property_syntax] = :new
when :normal; options[:property_syntax] = :old
end
options[:sourcemap] = :auto if options[:sourcemap] == true

options
end
Expand Down Expand Up @@ -351,7 +352,10 @@ def _render_with_sourcemap(sourcemap_uri)
Without a public URL, there's nothing for the source map to link to.
An importer was not set for this file.
ERR
elsif Sass::Util.silence_warnings {importer.public_url(filename, sourcemap_dir).nil?}
elsif Sass::Util.silence_warnings do
sourcemap_dir = nil if @options[:sourcemap] == :file
importer.public_url(filename, sourcemap_dir).nil?
end
raise Sass::SyntaxError.new(<<ERR)
Error generating source map: couldn't determine public URL for "#{filename}".
Without a public URL, there's nothing for the source map to link to.
Expand Down
11 changes: 2 additions & 9 deletions lib/sass/exec/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,8 @@ def encoding_option(opts)
end
end

# Processes the options set by the command-line arguments.
# In particular, sets `@options[:input]` and `@options[:output]`
# (and `@options[:sourcemap]` if one has been specified)
# to appropriate IO streams.
# Processes the options set by the command-line arguments. In particular,
# sets `@options[:input]` and `@options[:output]` to appropriate IO streams.
#
# This is meant to be overridden by subclasses
# so they can run their respective programs.
Expand All @@ -111,11 +109,6 @@ def process_result
end
@options[:output_filename] = args.shift
output ||= @options[:output_filename] || $stdout

if @options[:sourcemap] && @options[:output_filename]
@options[:sourcemap_filename] = Sass::Util.sourcemap_name(@options[:output_filename])
end

@options[:input], @options[:output] = input, output
end

Expand Down
108 changes: 63 additions & 45 deletions lib/sass/exec/sass_scss.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class SassScss < Base
# @param args [Array<String>] The command-line arguments
def initialize(args, default_syntax)
super(args)
@options[:sourcemap] = :auto
@options[:for_engine] = {
:load_paths => default_sass_path
}
Expand Down Expand Up @@ -48,51 +49,17 @@ def process_result
return interactive if @options[:interactive]
return watch_or_update if @options[:watch] || @options[:update]
super

if @options[:sourcemap] != :none && @options[:output_filename]
@options[:sourcemap_filename] = Sass::Util.sourcemap_name(@options[:output_filename])
end

@options[:for_engine][:filename] = @options[:filename]
@options[:for_engine][:css_filename] = @options[:output] if @options[:output].is_a?(String)
@options[:for_engine][:sourcemap_filename] = @options[:sourcemap_filename]
@options[:for_engine][:sourcemap] = @options[:sourcemap]

begin
input = @options[:input]
output = @options[:output]

@options[:for_engine][:syntax] ||= :scss if input.is_a?(File) && input.path =~ /\.scss$/
@options[:for_engine][:syntax] ||= @default_syntax
engine =
if input.is_a?(File) && !@options[:check_syntax]
Sass::Engine.for_file(input.path, @options[:for_engine])
else
# We don't need to do any special handling of @options[:check_syntax] here,
# because the Sass syntax checking happens alongside evaluation
# and evaluation doesn't actually evaluate any code anyway.
Sass::Engine.new(input.read, @options[:for_engine])
end

input.close if input.is_a?(File)

if @options[:sourcemap]
unless @options[:sourcemap_filename]
raise "Can't generate a sourcemap for an input without a path."
end

relative_sourcemap_path = Sass::Util.pathname(@options[:sourcemap_filename]).
relative_path_from(Sass::Util.pathname(@options[:output_filename]).dirname)
rendered, mapping = engine.render_with_sourcemap(relative_sourcemap_path.to_s)
write_output(rendered, output)
write_output(mapping.to_json(
:css_path => @options[:output_filename],
:sourcemap_path => @options[:sourcemap_filename]) + "\n",
@options[:sourcemap_filename])
else
write_output(engine.render, output)
end
rescue Sass::SyntaxError => e
write_output(Sass::SyntaxError.exception_to_css(e), output) if output.is_a?(String)
raise e if @options[:trace]
raise e.sass_backtrace_str("standard input")
ensure
output.close if output.is_a? File
end
run
end

private
Expand All @@ -101,10 +68,6 @@ def common_options(opts)
opts.separator ''
opts.separator 'Common Options:'

opts.on('--sourcemap', 'Create sourcemap files next to the generated CSS files.') do
@options[:sourcemap] = true
end

opts.on('-I', '--load-path PATH', 'Specify a Sass import path.') do |path|
@options[:for_engine][:load_paths] << path
end
Expand Down Expand Up @@ -184,6 +147,22 @@ def input_and_output(opts)
end
end

# This is optional for backwards-compatibility with Sass 3.3, which didn't
# enable sourcemaps by default and instead used "--sourcemap" to do so.
opts.on(:OPTIONAL, '--sourcemap=TYPE',
'How link generated output to the source files.',
' auto (default): relative paths where possible, file URIs elsewhere',
' file: always absolute file URIs',
' none: no sourcemaps') do |type|
if type && !%w[auto file none].include?(type)
$stderr.puts "Unknown sourcemap type #{type}.\n\n"
$stderr.puts opts
exit
end

@options[:sourcemap] = (type || :auto).to_sym
end

opts.on('-s', '--stdin', :NONE,
'Read input from standard input instead of an input file.',
'This is the default if no input file is specified.') do
Expand Down Expand Up @@ -365,6 +344,45 @@ def watch_or_update
# @comment
# rubocop:enable MethodLength

def run
input = @options[:input]
output = @options[:output]

@options[:for_engine][:syntax] ||= :scss if input.is_a?(File) && input.path =~ /\.scss$/
@options[:for_engine][:syntax] ||= @default_syntax
engine =
if input.is_a?(File) && !@options[:check_syntax]
Sass::Engine.for_file(input.path, @options[:for_engine])
else
# We don't need to do any special handling of @options[:check_syntax] here,
# because the Sass syntax checking happens alongside evaluation
# and evaluation doesn't actually evaluate any code anyway.
Sass::Engine.new(input.read, @options[:for_engine])
end

input.close if input.is_a?(File)

if @options[:sourcemap] != :none && @options[:sourcemap_filename]
relative_sourcemap_path = Sass::Util.pathname(@options[:sourcemap_filename]).
relative_path_from(Sass::Util.pathname(@options[:output_filename]).dirname)
rendered, mapping = engine.render_with_sourcemap(relative_sourcemap_path.to_s)
write_output(rendered, output)
write_output(mapping.to_json(
:type => @options[:sourcemap],
:css_path => @options[:output_filename],
:sourcemap_path => @options[:sourcemap_filename]) + "\n",
@options[:sourcemap_filename])
else
write_output(engine.render, output)
end
rescue Sass::SyntaxError => e
write_output(Sass::SyntaxError.exception_to_css(e), output) if output.is_a?(String)
raise e if @options[:trace]
raise e.sass_backtrace_str("standard input")
ensure
output.close if output.is_a? File
end

def colon_path?(path)
!split_colon_path(path)[1].nil?
end
Expand Down
6 changes: 5 additions & 1 deletion lib/sass/importers/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ def key(uri, options)
# sourcemap generation to fail if any CSS is generated from files imported
# from this importer.
#
# If an absolute "file:" URI can be produced for an imported file, that
# should be preferred to returning `nil`. However, a URL relative to
# `sourcemap_directory` should be preferred over an absolute "file:" URI.
#
# @param uri [String] A URI known to be valid for this importer.
# @param sourcemap_directory [String, NilClass] The absolute path to a
# directory on disk where the sourcemap will be saved. If uri refers to
Expand All @@ -132,7 +136,7 @@ def key(uri, options)
# eventual location is unknown.
# @return [String?] The publicly-visible URL for this file, or `nil`
# indicating that no publicly-visible URL exists.
def public_url(uri, sourcemap_directory = nil)
def public_url(uri, sourcemap_directory)
return if @public_url_warning_issued
@public_url_warning_issued = true
Sass::Util.sass_warn <<WARNING
Expand Down
25 changes: 4 additions & 21 deletions lib/sass/importers/filesystem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,16 @@ def watched_file?(filename)
filename.start_with?(root + File::SEPARATOR)
end

def public_url(name, sourcemap_directory = nil)
def public_url(name, sourcemap_directory)
file_pathname = Sass::Util.cleanpath(Sass::Util.absolute_path(name, @root))
if sourcemap_directory.nil?
warn_about_public_url(name)
Sass::Util.file_uri_from_path(file_pathname)
else
file_pathname = Sass::Util.cleanpath(Sass::Util.absolute_path(name, @root))
sourcemap_pathname = Sass::Util.cleanpath(sourcemap_directory)
begin
file_pathname.relative_path_from(sourcemap_pathname).to_s
rescue ArgumentError # when a relative path cannot be constructed
warn_about_public_url(name)
nil
Sass::Util.file_uri_from_path(file_pathname)
end
end
end
Expand Down Expand Up @@ -196,22 +195,6 @@ def split(name)
[dirname, basename, extension]
end

# Issues a warning about being unable to determine a public url.
#
# @param uri [String] A URI known to be valid for this importer.
# @return [NilClass] nil
def warn_about_public_url(uri)
@warnings_issued ||= Set.new
unless @warnings_issued.include?(uri)
Sass::Util.sass_warn <<WARNING
WARNING: Couldn't determine public URL for "#{uri}" while generating sourcemap.
Without a public URL, there's nothing for the source map to link to.
WARNING
@warnings_issued << uri
end
nil
end

private

def _find(dir, name, options)
Expand Down
5 changes: 3 additions & 2 deletions lib/sass/plugin/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def update_stylesheets(individual_files = [])
# Get the relative path to the file
name = file.sub(template_location.to_s.sub(/\/*$/, '/'), "")
css = css_filename(name, css_location)
sourcemap = Sass::Util.sourcemap_name(css) if engine_options[:sourcemap]
sourcemap = Sass::Util.sourcemap_name(css) if engine_options[:sourcemap] != :none
individual_files << [file, css, sourcemap]
end
end
Expand Down Expand Up @@ -386,7 +386,8 @@ def update_stylesheet(filename, css, sourcemap)

write_file(css, rendered)
if mapping
write_file(sourcemap, mapping.to_json(:css_path => css, :sourcemap_path => sourcemap))
write_file(sourcemap, mapping.to_json(
:css_path => css, :sourcemap_path => sourcemap, :type => options[:sourcemap]))
end
run_updated_stylesheet(filename, css, sourcemap) unless compilation_error_occured
end
Expand Down
14 changes: 6 additions & 8 deletions lib/sass/source/map.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,6 @@ def shift_output_offsets(delta)
# it will be inferred from `:css_path` and `:sourcemap_path` using the
# assumption that the local file system has the same layout as the server.
#
# If any source stylesheets use the default filesystem importer, sourcemap
# generation will fail unless the `:sourcemap_path` option is specified.
# The layout of the local file system is assumed to be the same as the
# layout of the server for the purposes of linking to source stylesheets
# that use the filesystem importer.
#
# Regardless of which options are passed to this method, source stylesheets
# that are imported using a non-default importer will only be linked to in
# the source map if their importers implement
Expand All @@ -85,6 +79,8 @@ def shift_output_offsets(delta)
# The local path of the CSS output file.
# @option options :sourcemap_path [String]
# The (eventual) local path of the sourcemap file.
# @option options :type [Symbol]
# `:auto` (default) or `:file`.
# @return [String] The JSON string.
# @raise [ArgumentError] If neither `:css_uri` nor `:css_path` and
# `:sourcemap_path` are specified.
Expand Down Expand Up @@ -119,8 +115,10 @@ def to_json(options)

@data.each do |m|
file, importer = m.input.file, m.input.importer
source_uri = importer &&
importer.public_url(file, sourcemap_path && sourcemap_path.dirname.to_s)

sourcemap_dir = sourcemap_path && sourcemap_path.dirname.to_s
sourcemap_dir = nil if options[:type] == :file
source_uri = importer && importer.public_url(file, sourcemap_dir)
next unless source_uri

current_source_id = source_uri_to_id[source_uri]
Expand Down
12 changes: 12 additions & 0 deletions lib/sass/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,18 @@ def cleanpath(path)
pathname(path.cleanpath.to_s)
end

# Converts `path` to a "file:" URI. This handles Windows paths correctly.
#
# @param path [String, Pathname]
# @return [String]
def file_uri_from_path(path)
path = path.to_s if path.is_a?(Pathname)
return path.start_with?('/') ? "file://" + path : path unless windows?
return "file:///" + path.tr("\\", "/") if path =~ /^[a-zA-Z]:[\/\\]/
return "file://" + path.tr("\\", "/") if path =~ /\\\\[^\\]+\\[^\\\/]+/
path.tr("\\", "/")
end

# Prepare a value for a destructuring assignment (e.g. `a, b =
# val`). This works around a performance bug when using
# ActiveSupport, and only needs to be called when `val` is likely
Expand Down
2 changes: 1 addition & 1 deletion test/sass/exec_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def test_scss_t_expanded
src = get_path("src.scss")
dest = get_path("dest.css")
write(src, ".ruleset { margin: 0 }")
assert(exec(*%w[scss -t expanded --unix-newlines].push(src, dest)))
assert(exec(*%w[scss --sourcemap=none -t expanded --unix-newlines].push(src, dest)))
assert_equal(".ruleset {\n margin: 0;\n}\n", read(dest))
end

Expand Down
Loading

0 comments on commit 4da1c1a

Please sign in to comment.