sprockets 3.7.2 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -267
- data/README.md +477 -321
- data/bin/sprockets +11 -7
- data/lib/rake/sprocketstask.rb +3 -2
- data/lib/sprockets.rb +99 -39
- data/lib/sprockets/add_source_map_comment_to_asset_processor.rb +60 -0
- data/lib/sprockets/asset.rb +31 -23
- data/lib/sprockets/autoload.rb +5 -0
- data/lib/sprockets/autoload/babel.rb +8 -0
- data/lib/sprockets/autoload/closure.rb +1 -0
- data/lib/sprockets/autoload/coffee_script.rb +1 -0
- data/lib/sprockets/autoload/eco.rb +1 -0
- data/lib/sprockets/autoload/ejs.rb +1 -0
- data/lib/sprockets/autoload/jsminc.rb +8 -0
- data/lib/sprockets/autoload/sass.rb +1 -0
- data/lib/sprockets/autoload/sassc.rb +8 -0
- data/lib/sprockets/autoload/uglifier.rb +1 -0
- data/lib/sprockets/autoload/yui.rb +1 -0
- data/lib/sprockets/autoload/zopfli.rb +7 -0
- data/lib/sprockets/babel_processor.rb +66 -0
- data/lib/sprockets/base.rb +49 -12
- data/lib/sprockets/bower.rb +5 -2
- data/lib/sprockets/bundle.rb +40 -4
- data/lib/sprockets/cache.rb +36 -1
- data/lib/sprockets/cache/file_store.rb +25 -3
- data/lib/sprockets/cache/memory_store.rb +9 -0
- data/lib/sprockets/cache/null_store.rb +8 -0
- data/lib/sprockets/cached_environment.rb +14 -19
- data/lib/sprockets/closure_compressor.rb +1 -0
- data/lib/sprockets/coffee_script_processor.rb +18 -4
- data/lib/sprockets/compressing.rb +43 -3
- data/lib/sprockets/configuration.rb +3 -7
- data/lib/sprockets/context.rb +97 -24
- data/lib/sprockets/dependencies.rb +1 -0
- data/lib/sprockets/digest_utils.rb +25 -5
- data/lib/sprockets/directive_processor.rb +45 -35
- data/lib/sprockets/eco_processor.rb +1 -0
- data/lib/sprockets/ejs_processor.rb +1 -0
- data/lib/sprockets/encoding_utils.rb +1 -0
- data/lib/sprockets/environment.rb +9 -4
- data/lib/sprockets/erb_processor.rb +28 -21
- data/lib/sprockets/errors.rb +1 -0
- data/lib/sprockets/exporters/base.rb +71 -0
- data/lib/sprockets/exporters/file_exporter.rb +24 -0
- data/lib/sprockets/exporters/zlib_exporter.rb +33 -0
- data/lib/sprockets/exporters/zopfli_exporter.rb +14 -0
- data/lib/sprockets/exporting.rb +73 -0
- data/lib/sprockets/file_reader.rb +1 -0
- data/lib/sprockets/http_utils.rb +25 -7
- data/lib/sprockets/jsminc_compressor.rb +32 -0
- data/lib/sprockets/jst_processor.rb +11 -10
- data/lib/sprockets/loader.rb +87 -67
- data/lib/sprockets/manifest.rb +64 -62
- data/lib/sprockets/manifest_utils.rb +9 -6
- data/lib/sprockets/mime.rb +8 -42
- data/lib/sprockets/npm.rb +52 -0
- data/lib/sprockets/path_dependency_utils.rb +3 -11
- data/lib/sprockets/path_digest_utils.rb +2 -1
- data/lib/sprockets/path_utils.rb +87 -7
- data/lib/sprockets/paths.rb +1 -0
- data/lib/sprockets/preprocessors/default_source_map.rb +49 -0
- data/lib/sprockets/processing.rb +31 -61
- data/lib/sprockets/processor_utils.rb +24 -35
- data/lib/sprockets/resolve.rb +177 -93
- data/lib/sprockets/sass_cache_store.rb +2 -6
- data/lib/sprockets/sass_compressor.rb +13 -1
- data/lib/sprockets/sass_functions.rb +1 -0
- data/lib/sprockets/sass_importer.rb +1 -0
- data/lib/sprockets/sass_processor.rb +30 -9
- data/lib/sprockets/sassc_compressor.rb +56 -0
- data/lib/sprockets/sassc_processor.rb +297 -0
- data/lib/sprockets/server.rb +26 -23
- data/lib/sprockets/source_map_processor.rb +66 -0
- data/lib/sprockets/source_map_utils.rb +483 -0
- data/lib/sprockets/transformers.rb +63 -35
- data/lib/sprockets/uglifier_compressor.rb +21 -11
- data/lib/sprockets/unloaded_asset.rb +13 -11
- data/lib/sprockets/uri_tar.rb +1 -0
- data/lib/sprockets/uri_utils.rb +11 -8
- data/lib/sprockets/utils.rb +41 -74
- data/lib/sprockets/utils/gzip.rb +46 -14
- data/lib/sprockets/version.rb +2 -1
- data/lib/sprockets/yui_compressor.rb +1 -0
- metadata +127 -23
- data/LICENSE +0 -21
- data/lib/sprockets/coffee_script_template.rb +0 -17
- data/lib/sprockets/deprecation.rb +0 -90
- data/lib/sprockets/eco_template.rb +0 -17
- data/lib/sprockets/ejs_template.rb +0 -17
- data/lib/sprockets/engines.rb +0 -92
- data/lib/sprockets/erb_template.rb +0 -11
- data/lib/sprockets/legacy.rb +0 -330
- data/lib/sprockets/legacy_proc_processor.rb +0 -35
- data/lib/sprockets/legacy_tilt_processor.rb +0 -29
- data/lib/sprockets/sass_template.rb +0 -19
data/lib/sprockets/server.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'set'
|
1
3
|
require 'time'
|
2
4
|
require 'rack/utils'
|
3
5
|
|
@@ -6,6 +8,9 @@ module Sprockets
|
|
6
8
|
# `CachedEnvironment` that provides a Rack compatible `call`
|
7
9
|
# interface and url generation helpers.
|
8
10
|
module Server
|
11
|
+
# Supported HTTP request methods.
|
12
|
+
ALLOWED_REQUEST_METHODS = ['GET', 'HEAD'].to_set.freeze
|
13
|
+
|
9
14
|
# `call` implements the Rack 1.x specification which accepts an
|
10
15
|
# `env` Hash and returns a three item tuple with the status code,
|
11
16
|
# headers, and body.
|
@@ -23,7 +28,7 @@ module Sprockets
|
|
23
28
|
start_time = Time.now.to_f
|
24
29
|
time_elapsed = lambda { ((Time.now.to_f - start_time) * 1000).to_i }
|
25
30
|
|
26
|
-
|
31
|
+
unless ALLOWED_REQUEST_METHODS.include? env['REQUEST_METHOD']
|
27
32
|
return method_not_allowed_response
|
28
33
|
end
|
29
34
|
|
@@ -32,6 +37,10 @@ module Sprockets
|
|
32
37
|
# Extract the path from everything after the leading slash
|
33
38
|
path = Rack::Utils.unescape(env['PATH_INFO'].to_s.sub(/^\//, ''))
|
34
39
|
|
40
|
+
unless path.valid_encoding?
|
41
|
+
return bad_request_response(env)
|
42
|
+
end
|
43
|
+
|
35
44
|
# Strip fingerprint
|
36
45
|
if fingerprint = path_fingerprint(path)
|
37
46
|
path = path.sub("-#{fingerprint}", '')
|
@@ -42,29 +51,19 @@ module Sprockets
|
|
42
51
|
return forbidden_response(env)
|
43
52
|
end
|
44
53
|
|
45
|
-
# Look up the asset.
|
46
|
-
options = {}
|
47
|
-
options[:pipeline] = :self if body_only?(env)
|
48
|
-
|
49
|
-
asset = find_asset(path, options)
|
50
|
-
|
51
|
-
# 2.x/3.x compatibility hack. Just ignore fingerprints on ?body=1 requests.
|
52
|
-
# 3.x/4.x prefers strong validation of fingerprint to body contents, but
|
53
|
-
# 2.x just ignored it.
|
54
|
-
if asset && parse_asset_uri(asset.uri)[1][:pipeline] == "self"
|
55
|
-
fingerprint = nil
|
56
|
-
end
|
57
|
-
|
58
54
|
if fingerprint
|
59
55
|
if_match = fingerprint
|
60
56
|
elsif env['HTTP_IF_MATCH']
|
61
|
-
if_match = env['HTTP_IF_MATCH'][
|
57
|
+
if_match = env['HTTP_IF_MATCH'][/"(\w+)"$/, 1]
|
62
58
|
end
|
63
59
|
|
64
60
|
if env['HTTP_IF_NONE_MATCH']
|
65
|
-
if_none_match = env['HTTP_IF_NONE_MATCH'][
|
61
|
+
if_none_match = env['HTTP_IF_NONE_MATCH'][/"(\w+)"$/, 1]
|
66
62
|
end
|
67
63
|
|
64
|
+
# Look up the asset.
|
65
|
+
asset = find_asset(path)
|
66
|
+
|
68
67
|
if asset.nil?
|
69
68
|
status = :not_found
|
70
69
|
elsif fingerprint && asset.etag != fingerprint
|
@@ -136,6 +135,15 @@ module Sprockets
|
|
136
135
|
[ 304, cache_headers(env, etag), [] ]
|
137
136
|
end
|
138
137
|
|
138
|
+
# Returns a 400 Forbidden response tuple
|
139
|
+
def bad_request_response(env)
|
140
|
+
if head_request?(env)
|
141
|
+
[ 400, { "Content-Type" => "text/plain", "Content-Length" => "0" }, [] ]
|
142
|
+
else
|
143
|
+
[ 400, { "Content-Type" => "text/plain", "Content-Length" => "11" }, [ "Bad Request" ] ]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
139
147
|
# Returns a 403 Forbidden response tuple
|
140
148
|
def forbidden_response(env)
|
141
149
|
if head_request?(env)
|
@@ -236,22 +244,17 @@ module Sprockets
|
|
236
244
|
gsub('/', '\\\\002f ')
|
237
245
|
end
|
238
246
|
|
239
|
-
# Test if `?body=1` or `body=true` query param is set
|
240
|
-
def body_only?(env)
|
241
|
-
env["QUERY_STRING"].to_s =~ /body=(1|t)/
|
242
|
-
end
|
243
|
-
|
244
247
|
def cache_headers(env, etag)
|
245
248
|
headers = {}
|
246
249
|
|
247
250
|
# Set caching headers
|
248
|
-
headers["Cache-Control"] = "public"
|
251
|
+
headers["Cache-Control"] = +"public"
|
249
252
|
headers["ETag"] = %("#{etag}")
|
250
253
|
|
251
254
|
# If the request url contains a fingerprint, set a long
|
252
255
|
# expires on the response
|
253
256
|
if path_fingerprint(env["PATH_INFO"])
|
254
|
-
headers["Cache-Control"] << ", max-age=31536000"
|
257
|
+
headers["Cache-Control"] << ", max-age=31536000, immutable"
|
255
258
|
|
256
259
|
# Otherwise set `must-revalidate` since the asset could be modified.
|
257
260
|
else
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Sprockets
|
5
|
+
|
6
|
+
# The purpose of this class is to generate a source map file
|
7
|
+
# that can be read and understood by browsers.
|
8
|
+
#
|
9
|
+
# When a file is passed in it will have a `application/js-sourcemap+json`
|
10
|
+
# or `application/css-sourcemap+json` mime type. The filename will be
|
11
|
+
# match the original asset. The original asset is loaded. As it
|
12
|
+
# gets processed by Sprockets it will aquire all information
|
13
|
+
# needed to build a source map file in the `asset.to_hash[:metadata][:map]`
|
14
|
+
# key.
|
15
|
+
#
|
16
|
+
# The output is an asset with a properly formatted source map file:
|
17
|
+
#
|
18
|
+
# {
|
19
|
+
# "version": 3,
|
20
|
+
# "sources": ["foo.js"],
|
21
|
+
# "names": [ ],
|
22
|
+
# "mappings": "AAAA,GAAIA"
|
23
|
+
# }
|
24
|
+
#
|
25
|
+
class SourceMapProcessor
|
26
|
+
def self.call(input)
|
27
|
+
links = Set.new(input[:metadata][:links])
|
28
|
+
env = input[:environment]
|
29
|
+
|
30
|
+
uri, _ = env.resolve!(input[:filename], accept: self.original_content_type(input[:content_type]))
|
31
|
+
asset = env.load(uri)
|
32
|
+
map = asset.metadata[:map]
|
33
|
+
|
34
|
+
# TODO: Because of the default piplene hack we have to apply dependencies
|
35
|
+
# from compiled asset to the source map, otherwise the source map cache
|
36
|
+
# will never detect the changes from directives
|
37
|
+
dependencies = Set.new(input[:metadata][:dependencies])
|
38
|
+
dependencies.merge(asset.metadata[:dependencies])
|
39
|
+
|
40
|
+
map["file"] = PathUtils.split_subpath(input[:load_path], input[:filename])
|
41
|
+
sources = map["sections"] ? map["sections"].map { |s| s["map"]["sources"] }.flatten : map["sources"]
|
42
|
+
|
43
|
+
sources.each do |source|
|
44
|
+
source = PathUtils.join(File.dirname(map["file"]), source)
|
45
|
+
uri, _ = env.resolve!(source)
|
46
|
+
links << uri
|
47
|
+
end
|
48
|
+
|
49
|
+
json = JSON.generate(map)
|
50
|
+
|
51
|
+
{ data: json, links: links, dependencies: dependencies }
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.original_content_type(source_map_content_type, error_when_not_found: true)
|
55
|
+
case source_map_content_type
|
56
|
+
when "application/js-sourcemap+json"
|
57
|
+
"application/javascript"
|
58
|
+
when "application/css-sourcemap+json"
|
59
|
+
"text/css"
|
60
|
+
else
|
61
|
+
fail(source_map_content_type) if error_when_not_found
|
62
|
+
source_map_content_type
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,483 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'json'
|
3
|
+
require 'sprockets/path_utils'
|
4
|
+
|
5
|
+
module Sprockets
|
6
|
+
module SourceMapUtils
|
7
|
+
extend self
|
8
|
+
|
9
|
+
# Public: Transpose source maps into a standard format
|
10
|
+
#
|
11
|
+
# NOTE: Does not support index maps
|
12
|
+
#
|
13
|
+
# version => 3
|
14
|
+
# file => logical path
|
15
|
+
# sources => relative from filename
|
16
|
+
#
|
17
|
+
# Unnecessary attributes are removed
|
18
|
+
#
|
19
|
+
# Example
|
20
|
+
#
|
21
|
+
# map
|
22
|
+
# #=> {
|
23
|
+
# # "version" => 3,
|
24
|
+
# # "file" => "stdin",
|
25
|
+
# # "sourceRoot" => "",
|
26
|
+
# # "sourceContents" => "blah blah blah",
|
27
|
+
# # "sources" => [/root/logical/path.js],
|
28
|
+
# # "names" => [..],
|
29
|
+
# #}
|
30
|
+
# format_source_map(map, input)
|
31
|
+
# #=> {
|
32
|
+
# # "version" => 3,
|
33
|
+
# # "file" => "logical/path.js",
|
34
|
+
# # "sources" => ["path.js"],
|
35
|
+
# # "names" => [..],
|
36
|
+
# #}
|
37
|
+
def format_source_map(map, input)
|
38
|
+
filename = input[:filename]
|
39
|
+
load_path = input[:load_path]
|
40
|
+
load_paths = input[:environment].config[:paths]
|
41
|
+
mime_exts = input[:environment].config[:mime_exts]
|
42
|
+
pipeline_exts = input[:environment].config[:pipeline_exts]
|
43
|
+
file = PathUtils.split_subpath(load_path, filename)
|
44
|
+
{
|
45
|
+
"version" => 3,
|
46
|
+
"file" => file,
|
47
|
+
"mappings" => map["mappings"],
|
48
|
+
"sources" => map["sources"].map do |source|
|
49
|
+
source = URIUtils.split_file_uri(source)[2] if source.start_with? "file://"
|
50
|
+
source = PathUtils.join(File.dirname(filename), source) unless PathUtils.absolute_path?(source)
|
51
|
+
_, source = PathUtils.paths_split(load_paths, source)
|
52
|
+
source = PathUtils.relative_path_from(file, source)
|
53
|
+
PathUtils.set_pipeline(source, mime_exts, pipeline_exts, :source)
|
54
|
+
end,
|
55
|
+
"names" => map["names"]
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Public: Concatenate two source maps.
|
60
|
+
#
|
61
|
+
# For an example, if two js scripts are concatenated, the individual source
|
62
|
+
# maps for those files can be concatenated to map back to the originals.
|
63
|
+
#
|
64
|
+
# Examples
|
65
|
+
#
|
66
|
+
# script3 = "#{script1}#{script2}"
|
67
|
+
# map3 = concat_source_maps(map1, map2)
|
68
|
+
#
|
69
|
+
# a - Source map hash
|
70
|
+
# b - Source map hash
|
71
|
+
#
|
72
|
+
# Returns a new source map hash.
|
73
|
+
def concat_source_maps(a, b)
|
74
|
+
return a || b unless a && b
|
75
|
+
a = make_index_map(a)
|
76
|
+
b = make_index_map(b)
|
77
|
+
|
78
|
+
offset = 0
|
79
|
+
if a["sections"].count != 0 && !a["sections"].last["map"]["mappings"].empty?
|
80
|
+
last_line_count = a["sections"].last["map"].delete("x_sprockets_linecount")
|
81
|
+
offset += last_line_count || 1
|
82
|
+
|
83
|
+
last_offset = a["sections"].last["offset"]["line"]
|
84
|
+
offset += last_offset
|
85
|
+
end
|
86
|
+
|
87
|
+
a["sections"] += b["sections"].map do |section|
|
88
|
+
{
|
89
|
+
"offset" => section["offset"].merge({ "line" => section["offset"]["line"] + offset }),
|
90
|
+
"map" => section["map"].merge({
|
91
|
+
"sources" => section["map"]["sources"].map do |source|
|
92
|
+
PathUtils.relative_path_from(a["file"], PathUtils.join(File.dirname(b["file"]), source))
|
93
|
+
end
|
94
|
+
})
|
95
|
+
}
|
96
|
+
end
|
97
|
+
a
|
98
|
+
end
|
99
|
+
|
100
|
+
# Public: Converts source map to index map
|
101
|
+
#
|
102
|
+
# Example:
|
103
|
+
#
|
104
|
+
# map
|
105
|
+
# # => {
|
106
|
+
# "version" => 3,
|
107
|
+
# "file" => "..",
|
108
|
+
# "mappings" => "AAAA;AACA;..;AACA",
|
109
|
+
# "sources" => [..],
|
110
|
+
# "names" => [..]
|
111
|
+
# }
|
112
|
+
# make_index_map(map)
|
113
|
+
# # => {
|
114
|
+
# "version" => 3,
|
115
|
+
# "file" => "..",
|
116
|
+
# "sections" => [
|
117
|
+
# {
|
118
|
+
# "offset" => { "line" => 0, "column" => 0 },
|
119
|
+
# "map" => {
|
120
|
+
# "version" => 3,
|
121
|
+
# "file" => "..",
|
122
|
+
# "mappings" => "AAAA;AACA;..;AACA",
|
123
|
+
# "sources" => [..],
|
124
|
+
# "names" => [..]
|
125
|
+
# }
|
126
|
+
# }
|
127
|
+
# ]
|
128
|
+
# }
|
129
|
+
def make_index_map(map)
|
130
|
+
return map if map.key? "sections"
|
131
|
+
{
|
132
|
+
"version" => map["version"],
|
133
|
+
"file" => map["file"],
|
134
|
+
"sections" => [
|
135
|
+
{
|
136
|
+
"offset" => { "line" => 0, "column" => 0 },
|
137
|
+
"map" => map
|
138
|
+
}
|
139
|
+
]
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
# Public: Combine two seperate source map transformations into a single
|
144
|
+
# mapping.
|
145
|
+
#
|
146
|
+
# Source transformations may happen in discrete steps producing separate
|
147
|
+
# source maps. These steps can be combined into a single mapping back to
|
148
|
+
# the source.
|
149
|
+
#
|
150
|
+
# For an example, CoffeeScript may transform a file producing a map. Then
|
151
|
+
# Uglifier processes the result and produces another map. The CoffeeScript
|
152
|
+
# map can be combined with the Uglifier map so the source lines of the
|
153
|
+
# minified output can be traced back to the original CoffeeScript file.
|
154
|
+
#
|
155
|
+
# Returns a source map hash.
|
156
|
+
def combine_source_maps(first, second)
|
157
|
+
return second unless first
|
158
|
+
|
159
|
+
_first = decode_source_map(first)
|
160
|
+
_second = decode_source_map(second)
|
161
|
+
|
162
|
+
new_mappings = []
|
163
|
+
|
164
|
+
_second[:mappings].each do |m|
|
165
|
+
first_line = bsearch_mappings(_first[:mappings], m[:original])
|
166
|
+
new_mappings << first_line.merge(generated: m[:generated]) if first_line
|
167
|
+
end
|
168
|
+
|
169
|
+
_first[:mappings] = new_mappings
|
170
|
+
|
171
|
+
encode_source_map(_first)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Public: Decompress source map
|
175
|
+
#
|
176
|
+
# Example:
|
177
|
+
#
|
178
|
+
# decode_source_map(map)
|
179
|
+
# # => {
|
180
|
+
# version: 3,
|
181
|
+
# file: "..",
|
182
|
+
# mappings: [
|
183
|
+
# { source: "..", generated: [0, 0], original: [0, 0], name: ".."}, ..
|
184
|
+
# ],
|
185
|
+
# sources: [..],
|
186
|
+
# names: [..]
|
187
|
+
# }
|
188
|
+
#
|
189
|
+
# map - Source map hash (v3 spec)
|
190
|
+
#
|
191
|
+
# Returns an uncompressed source map hash
|
192
|
+
def decode_source_map(map)
|
193
|
+
return nil unless map
|
194
|
+
|
195
|
+
mappings, sources, names = [], [], []
|
196
|
+
if map["sections"]
|
197
|
+
map["sections"].each do |s|
|
198
|
+
mappings += decode_source_map(s["map"])[:mappings].each do |m|
|
199
|
+
m[:generated][0] += s["offset"]["line"]
|
200
|
+
m[:generated][1] += s["offset"]["column"]
|
201
|
+
end
|
202
|
+
sources |= s["map"]["sources"]
|
203
|
+
names |= s["map"]["names"]
|
204
|
+
end
|
205
|
+
else
|
206
|
+
mappings = decode_vlq_mappings(map["mappings"], sources: map["sources"], names: map["names"])
|
207
|
+
sources = map["sources"]
|
208
|
+
names = map["names"]
|
209
|
+
end
|
210
|
+
{
|
211
|
+
version: 3,
|
212
|
+
file: map["file"],
|
213
|
+
mappings: mappings,
|
214
|
+
sources: sources,
|
215
|
+
names: names
|
216
|
+
}
|
217
|
+
end
|
218
|
+
|
219
|
+
# Public: Compress source map
|
220
|
+
#
|
221
|
+
# Example:
|
222
|
+
#
|
223
|
+
# encode_source_map(map)
|
224
|
+
# # => {
|
225
|
+
# "version" => 3,
|
226
|
+
# "file" => "..",
|
227
|
+
# "mappings" => "AAAA;AACA;..;AACA",
|
228
|
+
# "sources" => [..],
|
229
|
+
# "names" => [..]
|
230
|
+
# }
|
231
|
+
#
|
232
|
+
# map - Source map hash (uncompressed)
|
233
|
+
#
|
234
|
+
# Returns a compressed source map hash according to source map spec v3
|
235
|
+
def encode_source_map(map)
|
236
|
+
return nil unless map
|
237
|
+
{
|
238
|
+
"version" => map[:version],
|
239
|
+
"file" => map[:file],
|
240
|
+
"mappings" => encode_vlq_mappings(map[:mappings], sources: map[:sources], names: map[:names]),
|
241
|
+
"sources" => map[:sources],
|
242
|
+
"names" => map[:names]
|
243
|
+
}
|
244
|
+
end
|
245
|
+
|
246
|
+
# Public: Compare two source map offsets.
|
247
|
+
#
|
248
|
+
# Compatible with Array#sort.
|
249
|
+
#
|
250
|
+
# a - Array [line, column]
|
251
|
+
# b - Array [line, column]
|
252
|
+
#
|
253
|
+
# Returns -1 if a < b, 0 if a == b and 1 if a > b.
|
254
|
+
def compare_source_offsets(a, b)
|
255
|
+
diff = a[0] - b[0]
|
256
|
+
diff = a[1] - b[1] if diff == 0
|
257
|
+
|
258
|
+
if diff < 0
|
259
|
+
-1
|
260
|
+
elsif diff > 0
|
261
|
+
1
|
262
|
+
else
|
263
|
+
0
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# Public: Search Array of mappings for closest offset.
|
268
|
+
#
|
269
|
+
# mappings - Array of mapping Hash objects
|
270
|
+
# offset - Array [line, column]
|
271
|
+
#
|
272
|
+
# Returns mapping Hash object.
|
273
|
+
def bsearch_mappings(mappings, offset, from = 0, to = mappings.size - 1)
|
274
|
+
mid = (from + to) / 2
|
275
|
+
|
276
|
+
if from > to
|
277
|
+
return from < 1 ? nil : mappings[from-1]
|
278
|
+
end
|
279
|
+
|
280
|
+
case compare_source_offsets(offset, mappings[mid][:generated])
|
281
|
+
when 0
|
282
|
+
mappings[mid]
|
283
|
+
when -1
|
284
|
+
bsearch_mappings(mappings, offset, from, mid - 1)
|
285
|
+
when 1
|
286
|
+
bsearch_mappings(mappings, offset, mid + 1, to)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Public: Decode VLQ mappings and match up sources and symbol names.
|
291
|
+
#
|
292
|
+
# str - VLQ string from 'mappings' attribute
|
293
|
+
# sources - Array of Strings from 'sources' attribute
|
294
|
+
# names - Array of Strings from 'names' attribute
|
295
|
+
#
|
296
|
+
# Returns an Array of Mappings.
|
297
|
+
def decode_vlq_mappings(str, sources: [], names: [])
|
298
|
+
mappings = []
|
299
|
+
|
300
|
+
source_id = 0
|
301
|
+
original_line = 1
|
302
|
+
original_column = 0
|
303
|
+
name_id = 0
|
304
|
+
|
305
|
+
vlq_decode_mappings(str).each_with_index do |group, index|
|
306
|
+
generated_column = 0
|
307
|
+
generated_line = index + 1
|
308
|
+
|
309
|
+
group.each do |segment|
|
310
|
+
generated_column += segment[0]
|
311
|
+
generated = [generated_line, generated_column]
|
312
|
+
|
313
|
+
if segment.size >= 4
|
314
|
+
source_id += segment[1]
|
315
|
+
original_line += segment[2]
|
316
|
+
original_column += segment[3]
|
317
|
+
|
318
|
+
source = sources[source_id]
|
319
|
+
original = [original_line, original_column]
|
320
|
+
else
|
321
|
+
# TODO: Research this case
|
322
|
+
next
|
323
|
+
end
|
324
|
+
|
325
|
+
if segment[4]
|
326
|
+
name_id += segment[4]
|
327
|
+
name = names[name_id]
|
328
|
+
end
|
329
|
+
|
330
|
+
mapping = {source: source, generated: generated, original: original}
|
331
|
+
mapping[:name] = name if name
|
332
|
+
mappings << mapping
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
mappings
|
337
|
+
end
|
338
|
+
|
339
|
+
# Public: Encode mappings Hash into a VLQ encoded String.
|
340
|
+
#
|
341
|
+
# mappings - Array of Hash mapping objects
|
342
|
+
# sources - Array of String sources (default: mappings source order)
|
343
|
+
# names - Array of String names (default: mappings name order)
|
344
|
+
#
|
345
|
+
# Returns a VLQ encoded String.
|
346
|
+
def encode_vlq_mappings(mappings, sources: nil, names: nil)
|
347
|
+
sources ||= mappings.map { |m| m[:source] }.uniq.compact
|
348
|
+
names ||= mappings.map { |m| m[:name] }.uniq.compact
|
349
|
+
|
350
|
+
sources_index = Hash[sources.each_with_index.to_a]
|
351
|
+
names_index = Hash[names.each_with_index.to_a]
|
352
|
+
|
353
|
+
source_id = 0
|
354
|
+
source_line = 1
|
355
|
+
source_column = 0
|
356
|
+
name_id = 0
|
357
|
+
|
358
|
+
by_lines = mappings.group_by { |m| m[:generated][0] }
|
359
|
+
|
360
|
+
ary = (1..(by_lines.keys.max || 1)).map do |line|
|
361
|
+
generated_column = 0
|
362
|
+
|
363
|
+
(by_lines[line] || []).map do |mapping|
|
364
|
+
group = []
|
365
|
+
group << mapping[:generated][1] - generated_column
|
366
|
+
group << sources_index[mapping[:source]] - source_id
|
367
|
+
group << mapping[:original][0] - source_line
|
368
|
+
group << mapping[:original][1] - source_column
|
369
|
+
group << names_index[mapping[:name]] - name_id if mapping[:name]
|
370
|
+
|
371
|
+
generated_column = mapping[:generated][1]
|
372
|
+
source_id = sources_index[mapping[:source]]
|
373
|
+
source_line = mapping[:original][0]
|
374
|
+
source_column = mapping[:original][1]
|
375
|
+
name_id = names_index[mapping[:name]] if mapping[:name]
|
376
|
+
|
377
|
+
group
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
vlq_encode_mappings(ary)
|
382
|
+
end
|
383
|
+
|
384
|
+
# Public: Base64 VLQ encoding
|
385
|
+
#
|
386
|
+
# Adopted from ConradIrwin/ruby-source_map
|
387
|
+
# https://github.com/ConradIrwin/ruby-source_map/blob/master/lib/source_map/vlq.rb
|
388
|
+
#
|
389
|
+
# Resources
|
390
|
+
#
|
391
|
+
# http://en.wikipedia.org/wiki/Variable-length_quantity
|
392
|
+
# https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
|
393
|
+
# https://github.com/mozilla/source-map/blob/master/lib/source-map/base64-vlq.js
|
394
|
+
#
|
395
|
+
VLQ_BASE_SHIFT = 5
|
396
|
+
VLQ_BASE = 1 << VLQ_BASE_SHIFT
|
397
|
+
VLQ_BASE_MASK = VLQ_BASE - 1
|
398
|
+
VLQ_CONTINUATION_BIT = VLQ_BASE
|
399
|
+
|
400
|
+
BASE64_DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
|
401
|
+
BASE64_VALUES = (0...64).inject({}) { |h, i| h[BASE64_DIGITS[i]] = i; h }
|
402
|
+
|
403
|
+
# Public: Encode a list of numbers into a compact VLQ string.
|
404
|
+
#
|
405
|
+
# ary - An Array of Integers
|
406
|
+
#
|
407
|
+
# Returns a VLQ String.
|
408
|
+
def vlq_encode(ary)
|
409
|
+
result = []
|
410
|
+
ary.each do |n|
|
411
|
+
vlq = n < 0 ? ((-n) << 1) + 1 : n << 1
|
412
|
+
loop do
|
413
|
+
digit = vlq & VLQ_BASE_MASK
|
414
|
+
vlq >>= VLQ_BASE_SHIFT
|
415
|
+
digit |= VLQ_CONTINUATION_BIT if vlq > 0
|
416
|
+
result << BASE64_DIGITS[digit]
|
417
|
+
|
418
|
+
break unless vlq > 0
|
419
|
+
end
|
420
|
+
end
|
421
|
+
result.join
|
422
|
+
end
|
423
|
+
|
424
|
+
# Public: Decode a VLQ string.
|
425
|
+
#
|
426
|
+
# str - VLQ encoded String
|
427
|
+
#
|
428
|
+
# Returns an Array of Integers.
|
429
|
+
def vlq_decode(str)
|
430
|
+
result = []
|
431
|
+
shift = 0
|
432
|
+
value = 0
|
433
|
+
i = 0
|
434
|
+
|
435
|
+
while i < str.size do
|
436
|
+
digit = BASE64_VALUES[str[i]]
|
437
|
+
raise ArgumentError unless digit
|
438
|
+
continuation = (digit & VLQ_CONTINUATION_BIT) != 0
|
439
|
+
digit &= VLQ_BASE_MASK
|
440
|
+
value += digit << shift
|
441
|
+
if continuation
|
442
|
+
shift += VLQ_BASE_SHIFT
|
443
|
+
else
|
444
|
+
result << ((value & 1) == 1 ? -(value >> 1) : value >> 1)
|
445
|
+
value = shift = 0
|
446
|
+
end
|
447
|
+
i += 1
|
448
|
+
end
|
449
|
+
result
|
450
|
+
end
|
451
|
+
|
452
|
+
# Public: Encode a mapping array into a compact VLQ string.
|
453
|
+
#
|
454
|
+
# ary - Two dimensional Array of Integers.
|
455
|
+
#
|
456
|
+
# Returns a VLQ encoded String seperated by , and ;.
|
457
|
+
def vlq_encode_mappings(ary)
|
458
|
+
ary.map { |group|
|
459
|
+
group.map { |segment|
|
460
|
+
vlq_encode(segment)
|
461
|
+
}.join(',')
|
462
|
+
}.join(';')
|
463
|
+
end
|
464
|
+
|
465
|
+
# Public: Decode a VLQ string into mapping numbers.
|
466
|
+
#
|
467
|
+
# str - VLQ encoded String
|
468
|
+
#
|
469
|
+
# Returns an two dimensional Array of Integers.
|
470
|
+
def vlq_decode_mappings(str)
|
471
|
+
mappings = []
|
472
|
+
|
473
|
+
str.split(';').each_with_index do |group, index|
|
474
|
+
mappings[index] = []
|
475
|
+
group.split(',').each do |segment|
|
476
|
+
mappings[index] << vlq_decode(segment)
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
mappings
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|