Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Sourcemaps #515

Merged
merged 23 commits into from
Nov 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ language: ruby
cache: bundler

sudo: false

before_script: "unset _JAVA_OPTIONS"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leaked in?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rvm:
- 2.2
- 2.3.4
Expand Down
12 changes: 7 additions & 5 deletions lib/sprockets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,6 @@ module Sprockets
[SourceMapCommentProcessor]
end

require 'sprockets/preprocessors/default_source_map'
register_preprocessor 'text/css', Preprocessors::DefaultSourceMap.new
register_preprocessor 'application/javascript', Preprocessors::DefaultSourceMap.new

require 'sprockets/directive_processor'
register_preprocessor 'text/css', DirectiveProcessor.new(comments: ["//", ["/*", "*/"]])
register_preprocessor 'application/javascript', DirectiveProcessor.new(comments: ["//", ["/*", "*/"]])
Expand All @@ -123,7 +119,6 @@ module Sprockets
register_bundle_metadata_reducer 'application/javascript', :data, proc { String.new("") }, Utils.method(:concat_javascript_sources)
register_bundle_metadata_reducer '*/*', :links, :+
register_bundle_metadata_reducer '*/*', :sources, proc { [] }, :+
register_bundle_metadata_reducer '*/*', :map, proc { |input| { "version" => 3, "file" => PathUtils.split_subpath(input[:load_path], input[:filename]), "sections" => [] } }, SourceMapUtils.method(:concat_source_maps)

require 'sprockets/closure_compressor'
require 'sprockets/sass_compressor'
Expand Down Expand Up @@ -219,4 +214,11 @@ module Sprockets

depend_on 'environment-version'
depend_on 'environment-paths'

require 'sprockets/preprocessors/default_source_map'
register_preprocessor 'text/css', Preprocessors::DefaultSourceMap.new
register_preprocessor 'application/javascript', Preprocessors::DefaultSourceMap.new

register_bundle_metadata_reducer 'text/css', :map, proc { |input| { "version" => 3, "file" => PathUtils.split_subpath(input[:load_path], input[:filename]), "sections" => [] } }, SourceMapUtils.method(:concat_source_maps)
register_bundle_metadata_reducer 'application/javascript', :map, proc { |input| { "version" => 3, "file" => PathUtils.split_subpath(input[:load_path], input[:filename]), "sections" => [] } }, SourceMapUtils.method(:concat_source_maps)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

end
2 changes: 1 addition & 1 deletion lib/sprockets/cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class Cache
# Internal: Cache key version for this class. Rarely should have to change
# unless the cache format radically changes. Will be bump on major version
# releases though.
VERSION = '4.0'
VERSION = '4.0.0'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this because concat_source_maps expects a x_sprockets_linecount key in the map metadata. It wasn’t put there until this PR, so if a map gets pulled out of the old cache and attempts to get loaded into memory then it will fail.

By rev-ing the cache we can ensure the key will be there, since it’s added by DefaultSourceMap.


def self.default_logger
logger = Logger.new($stderr)
Expand Down
8 changes: 6 additions & 2 deletions lib/sprockets/preprocessors/default_source_map.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def call(input)
map = input[:metadata][:map]
filename = input[:filename]
load_path = input[:load_path]
lines = input[:data].lines.count
lines = input[:data].lines.length
basename = File.basename(filename)
mime_exts = input[:environment].config[:mime_exts]
pipeline_exts = input[:environment].config[:pipeline_exts]
Expand All @@ -25,10 +25,14 @@ def call(input)
"sources" => [PathUtils.set_pipeline(basename, mime_exts, pipeline_exts, :source)],
"names" => []
}
else
result[:map] = map
end

result[:map]["x_sprockets_linecount"] = lines
return result
end

private

def default_mappings(lines)
Expand Down
2 changes: 1 addition & 1 deletion lib/sprockets/source_map_comment_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def self.call(input)
path = PathUtils.relative_path_from(PathUtils.split_subpath(input[:load_path], uri), map.digest_path)

asset.metadata.merge(
data: asset.source + (comment % path),
data: asset.source + (comment % path) + "\n",
links: asset.links + [asset.uri, map.uri]
)
end
Expand Down
17 changes: 10 additions & 7 deletions lib/sprockets/source_map_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def format_source_map(map, input)
"sources" => map["sources"].map do |source|
source = URIUtils.split_file_uri(source)[2] if source.start_with? "file://"
source = PathUtils.join(File.dirname(filename), source) unless PathUtils.absolute_path?(source)
_, source = PathUtils.paths_split(load_paths, source)
_, source = PathUtils.paths_split(load_paths, source)
source = PathUtils.relative_path_from(file, source)
PathUtils.set_pipeline(source, mime_exts, pipeline_exts, :source)
end,
Expand All @@ -72,13 +72,16 @@ def format_source_map(map, input)
# Returns a new source map hash.
def concat_source_maps(a, b)
return a || b unless a && b
a, b = make_index_map(a), make_index_map(b)
a = make_index_map(a)
b = make_index_map(b)

if a["sections"].count == 0 || a["sections"].last["map"]["mappings"].empty?
offset = 0
else
offset = a["sections"].last["map"]["mappings"].count(';') +
a["sections"].last["offset"]["line"] + 1
offset = 0
if a["sections"].count != 0 && !a["sections"].last["map"]["mappings"].empty?
last_line_count = a["sections"].last["map"].delete("x_sprockets_linecount")
offset += last_line_count

last_offset = a["sections"].last["offset"]["line"]
offset += last_offset
end

a["sections"] += b["sections"].map do |section|
Expand Down
41 changes: 21 additions & 20 deletions lib/sprockets/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ def hash_reassoc(hash, key_a, key_b = nil, &block)
end
end

WHITESPACE_ORDINALS = {0x0A => "\n", 0x20 => " ", 0x09 => "\t"}
private_constant :WHITESPACE_ORDINALS

# Internal: Check if string has a trailing semicolon.
#
# str - String
Expand All @@ -79,14 +82,9 @@ def string_end_with_semicolon?(str)
c = str[i].ord
i -= 1

# Need to compare against the ordinals because the string can be UTF_8 or UTF_32LE encoded
# 0x0A == "\n"
# 0x20 == " "
# 0x09 == "\t"
# 0x3B == ";"
unless c == 0x0A || c == 0x20 || c == 0x09
return c === 0x3B
end
next if WHITESPACE_ORDINALS[c]

return c === 0x3B
end

true
Expand All @@ -100,18 +98,21 @@ def string_end_with_semicolon?(str)
#
# Returns buf String.
def concat_javascript_sources(buf, source)
if source.bytesize > 0
buf << source

# If the source contains non-ASCII characters, indexing on it becomes O(N).
# This will lead to O(N^2) performance in string_end_with_semicolon?, so we should use 32 bit encoding to make sure indexing stays O(1)
source = source.encode(Encoding::UTF_32LE) unless source.ascii_only?

if !string_end_with_semicolon?(source)
buf << ";\n"
elsif source[source.size - 1].ord != 0x0A
buf << "\n"
end
return buf if source.bytesize <= 0

buf << source
# If the source contains non-ASCII characters, indexing on it becomes O(N).
# This will lead to O(N^2) performance in string_end_with_semicolon?, so we should use 32 bit encoding to make sure indexing stays O(1)
source = source.encode(Encoding::UTF_32LE) unless source.ascii_only?
return buf if string_end_with_semicolon?(source)

# If the last character in the string was whitespace,
# such as a newline, then we want to put the semicolon
# before the whitespace. Otherwise append a semicolon.
if whitespace = WHITESPACE_ORDINALS[source[-1].ord]
buf[-1] = ";#{whitespace}"
else
buf << ";"
end

buf
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/source-maps/multi-require.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//= require child.js
//= require coffee/main.js
//= require sub/a.js
//= require plain.js
3 changes: 3 additions & 0 deletions test/fixtures/source-maps/plain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function plain(input) {
console.log("Plain" + input)
}
48 changes: 37 additions & 11 deletions test/test_asset.rb
Original file line number Diff line number Diff line change
Expand Up @@ -885,8 +885,7 @@ def setup

define("application.js", "application-955b2dddd0d1449b1c617124b83b46300edadec06d561104f7f6165241b31a94.js")
define("application.css", "application-46d50149c56fc370805f53c29f79b89a52d4cc479eeebcdc8db84ab116d7ab1a.css")
define("POW.png", "POW-1da2e59df75d33d8b74c3d71feede698f203f136512cbaab20c68a5bdebd5800.png")
;
define("POW.png", "POW-1da2e59df75d33d8b74c3d71feede698f203f136512cbaab20c68a5bdebd5800.png");
EOS
assert_equal [
"file://#{fixture_path_for_uri("asset/POW.png")}?type=image/png&id=xxx",
Expand All @@ -904,8 +903,7 @@ def setup

define("application.js", "application-955b2dddd0d1449b1c617124b83b46300edadec06d561104f7f6165241b31a94.js")
define("application.css", "application-46d50149c56fc370805f53c29f79b89a52d4cc479eeebcdc8db84ab116d7ab1a.css")
define("POW.png", "POW-1da2e59df75d33d8b74c3d71feede698f203f136512cbaab20c68a5bdebd5800.png")
;
define("POW.png", "POW-1da2e59df75d33d8b74c3d71feede698f203f136512cbaab20c68a5bdebd5800.png");
EOS

assert_equal [
Expand Down Expand Up @@ -1080,13 +1078,22 @@ def setup
end

test "appends missing semicolons" do
assert_equal "var Bar\n;\n\n(function() {\n var Foo\n})\n;\n",
asset("semicolons.js").to_s
expected = <<-EOS
var Bar;

(function() {
var Foo
});
EOS
assert_equal expected, asset("semicolons.js").to_s
end

test 'keeps code in same line after multi-line comments' do
assert_equal "/******/ function foo() {\n}\n;\n",
asset('multi_line_comment.js').to_s
expected = <<-EOS
/******/ function foo() {
};
EOS
assert_equal expected, asset('multi_line_comment.js').to_s
end

test "should not fail if home is not set in environment" do
Expand Down Expand Up @@ -1134,7 +1141,7 @@ def setup
end

test "digest path" do
assert_equal "application.debug-60cfa762e3139c2580dbfbefd46a5359803225363eaef31efb725e6731ab6849.js",
assert_equal "application.debug-dee339ce0ee2dc7cdfa0d36dff3ef946cebe1bd3e414515d40c3cafc49c0a51a.js",
@asset.digest_path
end

Expand All @@ -1143,15 +1150,34 @@ def setup
end

test "length" do
assert_equal 264, @asset.length
assert_equal 265, @asset.length
end

test "charset is UTF-8" do
assert_equal 'utf-8', @asset.charset
end

test "to_s" do
assert_equal "var Project = {\n find: function(id) {\n }\n};\nvar Users = {\n find: function(id) {\n }\n};\n\n\n\ndocument.on('dom:loaded', function() {\n $('search').focus();\n});\n\n//# sourceMappingURL=application.js-161f7edec524c6e94ab3b89924c4fb96d4552bb83b32d975c074b7fb9a84c454.map", @asset.to_s
expected = <<-EOS
var Project = {
find: function(id) {
}
};
var Users = {
find: function(id) {
}
};



document.on('dom:loaded', function() {
$('search').focus();
});

//# sourceMappingURL=application.js-95e519d4e0f0a5c4c7d24787ded990b0d027f7ad30b39f402c4c5e3196a41e8b.map
EOS

assert_equal expected, @asset.to_s
end

def asset(logical_path, options = {})
Expand Down
8 changes: 5 additions & 3 deletions test/test_environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -798,10 +798,12 @@ def foo; end
end

test "disabling default directive preprocessor" do
assert processor = @env.preprocessors['application/javascript'][0]
assert_kind_of Sprockets::DirectiveProcessor, processor
assert processors = @env.preprocessors['application/javascript']
processor = processors.detect {|p| p.is_a?(Sprockets::DirectiveProcessor) }

assert processor
@env.unregister_preprocessor('application/javascript', processor)
assert_equal "// =require \"notfound\"\n;\n", @env["missing_require.js"].to_s
assert_equal "// =require \"notfound\";\n", @env["missing_require.js"].to_s
end

test "disabling processors by class name also disables processors which are instances of that class" do
Expand Down
4 changes: 2 additions & 2 deletions test/test_manifest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ class SlowExporter2 < SlowExporter
@env.register_exporter 'image/png',SlowExporter
@env.register_exporter 'image/png',SlowExporter2
Sprockets::Manifest.new(@env, @dir).compile('logo.png', 'troll.png')
assert_equal %w(0 0 0 0 1 1 1 1), SlowExporter.seq
refute_equal %w(0 1 0 1 0 1 0 1), SlowExporter.seq
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we asserting what this isn't instead of what it is?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we're exporting things serially then the second exporter will not start until the first one ends. We are guaranteed to get 0 1 0 1 0 . However when we're running this in parallel, then we do not need to wait for the first exporter to be finished for the second to run, etc. That's why the original code asserted that we got four zeroes first.

However that test is coupled to a sleep and therefore race conditions.

I changed the test to allow for other combinations, as long as they don't perfectly match the "synchronous" case.

end

test 'sequential exporting' do
Expand Down Expand Up @@ -642,7 +642,7 @@ def call(_)
processor = SlowProcessor.new
@env.register_postprocessor 'image/png', processor
Sprockets::Manifest.new(@env, @dir).compile('logo.png', 'troll.png')
assert_equal %w(0 0 1 1), processor.seq
refute_equal %w(0 1 0 1), processor.seq
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

end

test 'sequential processing' do
Expand Down
32 changes: 22 additions & 10 deletions test/test_source_map_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

class TestSourceMapUtils < MiniTest::Test

def deep_dup(object)
Marshal.load( Marshal.dump(object) )
end

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👹

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😇

def test_map
source_map = {
'version' => 3,
Expand Down Expand Up @@ -94,12 +98,15 @@ def test_concat_empty_source_map_returns_original
{ source: 'c.js', generated: [rand(1..100), rand(0..100)], original: [rand(1..100), rand(0..100)] }
].freeze

linecount = abc_mappings.count(";")
linecount += 1 if abc_mappings[-1] == ";"
map = {
"version" => 3,
"file" => "a.js",
"mappings" => Sprockets::SourceMapUtils.encode_vlq_mappings(abc_mappings),
"sources" => ["a.js", "b.js", "c.js"],
"names" => []
"names" => [],
"x_sprockets_linecount" => linecount
}
empty_map = { "version" => 3, "file" => 'a.js', "sections" => [] }
index_map = {
Expand All @@ -113,11 +120,14 @@ def test_concat_empty_source_map_returns_original
]
}

assert_equal map, Sprockets::SourceMapUtils.concat_source_maps(nil, map.dup)
assert_equal map, Sprockets::SourceMapUtils.concat_source_maps(map.dup, nil)
assert_equal map, Sprockets::SourceMapUtils.concat_source_maps(nil, deep_dup(map))
assert_equal map, Sprockets::SourceMapUtils.concat_source_maps(deep_dup(map), nil)

assert_equal index_map, Sprockets::SourceMapUtils.concat_source_maps(deep_dup(empty_map), map.dup)

assert_equal index_map, Sprockets::SourceMapUtils.concat_source_maps(empty_map.dup, map.dup)
assert_equal index_map, Sprockets::SourceMapUtils.concat_source_maps(map.dup, empty_map.dup)
expected_index_map = deep_dup(index_map)
expected_index_map["sections"].first["map"].delete("x_sprockets_linecount")
assert_equal expected_index_map, Sprockets::SourceMapUtils.concat_source_maps(deep_dup(map), deep_dup(empty_map))
end

def test_concat_source_maps
Expand All @@ -136,27 +146,29 @@ def test_concat_source_maps
"file" => "a.js",
"mappings" => Sprockets::SourceMapUtils.encode_vlq_mappings(abc_mappings),
"sources" => ["a.js", "b.js", "c.js"],
"names" => []
"names" => [],
"x_sprockets_linecount" => 3

}

d_map = {
"version" => 3,
"file" => "d.js",
"mappings" => Sprockets::SourceMapUtils.encode_vlq_mappings(d_mapping),
"sources" => ["d.js"],
"names" => []
"names" => [],
"x_sprockets_linecount" => 1
}


combined = Sprockets::SourceMapUtils.concat_source_maps(abc_map.dup, d_map.dup)
combined = Sprockets::SourceMapUtils.concat_source_maps(deep_dup(abc_map), deep_dup(d_map))
assert_equal [
{ source: 'a.js', generated: [1, 0], original: [1, 0] },
{ source: 'b.js', generated: [2, 0], original: [20, 0] },
{ source: 'c.js', generated: [3, 0], original: [30, 0] },
{ source: 'd.js', generated: [4, 0], original: [1, 0] }
], Sprockets::SourceMapUtils.decode_source_map(combined)[:mappings]

combined = Sprockets::SourceMapUtils.concat_source_maps(d_map.dup, abc_map.dup)
combined = Sprockets::SourceMapUtils.concat_source_maps(deep_dup(d_map), deep_dup(abc_map))
assert_equal [
{ source: 'd.js', generated: [1, 0], original: [1, 0] },
{ source: 'a.js', generated: [2, 0], original: [1, 0] },
Expand Down
Loading