-
Notifications
You must be signed in to change notification settings - Fork 790
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
Fix Sourcemaps #515
Conversation
The directive processor can add a newline to assets. If it comes after the default source map then this line is not accounted for when determining line offsets.
Adding a new line with semi-colons can cause off by 1 errors with source maps. Instead we want to add a semicolon. If the last character was whitespace such as a newline, we want to make sure that there are the same number of `str.lines` before and after, so we replace the whitespace character with a semicolon followed by the same whitespace character.
The calculation of length of asset is done by counting semicolons, however not all mappings end with a semicolon. When they do we must increment the offset. The next part is harder to explain. We concatenate two assets A.js and B.js. A has 10 lines B has 5 lines. After we’ve done this we want an offset that looks like this: ``` A.js: {"line"=>0, "column"=>0} B.js: {"line"=>11, "column"=>0} ``` To get 11 we take the prior length of mappings for A.js which is 10 (from the lines) and increment by one to get to 11. Now we add another asset C.js which has 2 lines. We want an offset that looks like this ``` A.js: {"line"=>0, "column"=>0} B.js: {"line"=>11, "column"=>0} C.js: {"line"=>16, "column"=>0} ``` If we add the last offset which is 11, to the length of the asset of B (which is 5) then we get the correct offset which is 16. We do not have to add an extra +1. I think this was accidentally working previously based on unwanted behavior from adding semi-colons and newlines.
This patch preserves an original source maps if one was present.
``` Warming up -------------------------------------- count 317.318k i/100ms length 329.512k i/100ms size 329.953k i/100ms Calculating ------------------------------------- count 9.806M (± 7.7%) i/s - 48.867M in 5.014627s length 11.696M (± 6.3%) i/s - 58.324M in 5.008040s size 11.074M (± 7.5%) i/s - 55.102M in 5.005626s ```
Based on the spec it is not guaranteed that there will be a mapping for each and every line https://groups.google.com/forum/#!topic/mozilla.dev.js-sourcemap/gp_ULp-h1fQ We were previously using this behavior to know the length of a previous asset when concatenating source maps. We can get around this by storing the length of the previous asset in an “x_sprockets_linecount” key. This is guaranteed to be on the asset since it is added by the `DefaultSourceMap`. We no longer need to offset the first asset by one line. This was accidentally working before.
We no longer add a newline if one did not previously exist.
Previous test was failing intermittently with results like `["0", "0", "1", "1", "0", "0", "1", “1”]`
c97c91d
to
f2bbb41
Compare
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`.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great batch of fixes ✌🏼
@@ -3,7 +3,7 @@ language: ruby | |||
cache: bundler | |||
|
|||
sudo: false | |||
|
|||
before_script: "unset _JAVA_OPTIONS" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leaked in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, causes a failure travis-ci/travis-ci#8408 (comment) and documentcloud/closure-compiler#48
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@@ -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' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated?
There was a problem hiding this comment.
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
.
lib/sprockets/utils.rb
Outdated
@@ -100,18 +98,18 @@ def string_end_with_semicolon?(str) | |||
# | |||
# Returns buf String. | |||
def concat_javascript_sources(buf, source) | |||
if source.bytesize > 0 | |||
buf << source | |||
return buf if source.bytesize < 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<=
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 💯
lib/sprockets/utils.rb
Outdated
elsif source[source.size - 1].ord != 0x0A | ||
buf << "\n" | ||
end | ||
if whitespace = WHITESPACE_ORDINALS[buf[-1].ord] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indexing into buf
defeats the purpose of source
encoding above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is a bug. I'm supposed to index into source
thanks and good catch!
lib/sprockets/utils.rb
Outdated
if whitespace = WHITESPACE_ORDINALS[buf[-1].ord] | ||
buf[-1] = ";#{whitespace}" | ||
else | ||
buf << ";" | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is officially a "there be dragons" method. Your PR commentary would be appropriate for discussion in code comments here, too, so it's clear what conditions are at play and where they're implemented.
Switching from conditional expressions to conditional control flow makes this simpler to read in imperative style, but it's harder (for me) to parse and understand.
@@ -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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
def deep_dup(object) | ||
Marshal.load( Marshal.dump(object) ) | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👹
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😇
So we can take advantage of the UTF32 encoding.
Sorry for the large PR, these issues all cropped up at the same time.
Problem: Concat JS adds a newline after semi-colon
Solution: Don't add a newline. If one exists put semi-colon before newline
Problem: Directive Processor adds newline between assets
This was done after the "Default Sourcemap" processor so it wasn't encoded in the asset's "map"
Solution: Move the default sourcemap processor to be last
Problem: Default source map processor doesn't preserve source maps
Solution: Add the map back to the returned result
Problem: Assets with sourcemap comment do not end in newline
Solution: Add newline after sourcemap comment
(Not required, but it makes things nicer)
Problem: Concatenating Source Maps is broken
To concatenate 2 source maps you can use multiple "sections" and give each section an offset. To calculate the offset, you need to know the previous offset, and the length of the last generated file. The offset can be pulled from the last map. The size of the prior generated file was calculated using the number of semicolons in the "mappings"
sprockets/lib/sprockets/source_map_utils.rb
Line 80 in 341fed4
This behavior is not explicitly stated in the spec, but is implied and confirmed that it should work: https://groups.google.com/forum/#!topic/mozilla.dev.js-sourcemap/gp_ULp-h1fQ. However it appears that coffeescript generated sourcemaps do not always have the same number of semicolons as the generated files. I've opened an issue and with coffeescript jashkenas/coffeescript#4786
Solution: To get around this, I've added an
x_sprockets_linecount
key to the sourcemaps that includes the line count of the generated file. This key is added in theDefaultSourceMap
which all assets will be processed through.The downside of this approach is this now means that the concat source map function will only work on files that have been passed through the
DefaultSourceMap
processor.