uglifier 1.3.0 → 4.2.1

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.
data/lib/uglifier.rb CHANGED
@@ -1,163 +1,519 @@
1
1
  # encoding: UTF-8
2
2
 
3
+ require "json"
3
4
  require "execjs"
4
- require "multi_json"
5
+ require "uglifier/version"
5
6
 
7
+ # A wrapper around the UglifyJS interface
6
8
  class Uglifier
7
- Error = ExecJS::Error
8
- # MultiJson.engine = :json_gem
9
+ # Error class for compilation errors.
10
+ class Error < StandardError; end
11
+
12
+ # UglifyJS source path
13
+ SourcePath = File.expand_path("../uglify.js", __FILE__)
14
+ # UglifyJS with Harmony source path
15
+ HarmonySourcePath = File.expand_path("../uglify-harmony.js", __FILE__)
16
+ # Source Map path
17
+ SourceMapPath = File.expand_path("../source-map.js", __FILE__)
18
+ # ES5 shims source path
19
+ ES5FallbackPath = File.expand_path("../es5.js", __FILE__)
20
+ # String.split shim source path
21
+ SplitFallbackPath = File.expand_path("../split.js", __FILE__)
22
+ # UglifyJS wrapper path
23
+ UglifyJSWrapperPath = File.expand_path("../uglifier.js", __FILE__)
9
24
 
10
25
  # Default options for compilation
11
26
  DEFAULTS = {
12
- :mangle => true, # Mangle variable and function names, use :vars to skip function mangling
13
- :toplevel => false, # Mangle top-level variable names
14
- :except => ["$super"], # Variable names to be excluded from mangling
15
- :max_line_length => 32 * 1024, # Maximum line length
16
- :squeeze => true, # Squeeze code resulting in smaller, but less-readable code
17
- :seqs => true, # Reduce consecutive statements in blocks into single statement
18
- :dead_code => true, # Remove dead code (e.g. after return)
19
- :lift_vars => false, # Lift all var declarations at the start of the scope
20
- :unsafe => false, # Optimizations known to be unsafe in some situations
21
- :copyright => true, # Show copyright message
22
- :ascii_only => false, # Encode non-ASCII characters as Unicode code points
23
- :inline_script => false, # Escape </script
24
- :quote_keys => false, # Quote keys in object literals
27
+ # rubocop:disable Layout/LineLength
28
+ :output => {
29
+ :ascii_only => true, # Escape non-ASCII characters
30
+ :comments => :copyright, # Preserve comments (:all, :jsdoc, :copyright, :none)
31
+ :inline_script => false, # Escape occurrences of </script in strings
32
+ :quote_keys => false, # Quote keys in object literals
33
+ :max_line_len => 32 * 1024, # Maximum line length in minified code
34
+ :bracketize => false, # Bracketize if, for, do, while or with statements, even if their body is a single statement
35
+ :semicolons => true, # Separate statements with semicolons
36
+ :preserve_line => false, # Preserve line numbers in outputs
37
+ :beautify => false, # Beautify output
38
+ :indent_level => 4, # Indent level in spaces
39
+ :indent_start => 0, # Starting indent level
40
+ :width => 80, # Specify line width when beautifier is used (only with beautifier)
41
+ :preamble => nil, # Preamble for the generated JS file. Can be used to insert any code or comment.
42
+ :wrap_iife => false, # Wrap IIFEs in parenthesis. Note: this disables the negate_iife compression option.
43
+ :shebang => true, # Preserve shebang (#!) in preamble (shell scripts)
44
+ :quote_style => 0, # Quote style, possible values :auto (default), :single, :double, :original
45
+ :keep_quoted_props => false # Keep quotes property names
46
+ },
47
+ :mangle => {
48
+ :eval => false, # Mangle names when eval of when is used in scope
49
+ :reserved => ["$super"], # Argument names to be excluded from mangling
50
+ :properties => false, # Mangle property names
51
+ :toplevel => false # Mangle names declared in the toplevel scope
52
+ }, # Mangle variable and function names, set to false to skip mangling
53
+ :compress => {
54
+ :sequences => true, # Allow statements to be joined by commas
55
+ :properties => true, # Rewrite property access using the dot notation
56
+ :dead_code => true, # Remove unreachable code
57
+ :drop_debugger => true, # Remove debugger; statements
58
+ :unsafe => false, # Apply "unsafe" transformations
59
+ :unsafe_comps => false, # Reverse < and <= to > and >= to allow improved compression. This might be unsafe when an at least one of two operands is an object with computed values due the use of methods like get, or valueOf. This could cause change in execution order after operands in the comparison are switching. Compression only works if both comparisons and unsafe_comps are both set to true.
60
+ :unsafe_math => false, # Optimize numerical expressions like 2 * x * 3 into 6 * x, which may give imprecise floating point results.
61
+ :unsafe_proto => false, # Optimize expressions like Array.prototype.slice.call(a) into [].slice.call(a)
62
+ :conditionals => true, # Optimize for if-s and conditional expressions
63
+ :comparisons => true, # Apply binary node optimizations for comparisons
64
+ :evaluate => true, # Attempt to evaluate constant expressions
65
+ :booleans => true, # Various optimizations to boolean contexts
66
+ :loops => true, # Optimize loops when condition can be statically determined
67
+ :unused => true, # Drop unreferenced functions and variables (simple direct variable assignments do not count as references unless set to `"keep_assign"`)
68
+ :toplevel => false, # Drop unreferenced top-level functions and variables
69
+ :top_retain => [], # prevent specific toplevel functions and variables from `unused` removal
70
+ :hoist_funs => true, # Hoist function declarations
71
+ :hoist_vars => false, # Hoist var declarations
72
+ :if_return => true, # Optimizations for if/return and if/continue
73
+ :join_vars => true, # Join consecutive var statements
74
+ :collapse_vars => true, # Collapse single-use var and const definitions when possible.
75
+ :reduce_funcs => false, # Inline single-use functions as function expressions. Depends on reduce_vars.
76
+ :reduce_vars => false, # Collapse variables assigned with and used as constant values.
77
+ :negate_iife => true, # Negate immediately invoked function expressions to avoid extra parens
78
+ :pure_getters => false, # Assume that object property access does not have any side-effects
79
+ :pure_funcs => nil, # List of functions without side-effects. Can safely discard function calls when the result value is not used
80
+ :drop_console => false, # Drop calls to console.* functions
81
+ :keep_fargs => false, # Preserve unused function arguments
82
+ :keep_fnames => false, # Do not drop names in function definitions
83
+ :passes => 1, # Number of times to run compress. Raising the number of passes will increase compress time, but can produce slightly smaller code.
84
+ :keep_infinity => false, # Prevent compression of Infinity to 1/0
85
+ :side_effects => true, # Pass false to disable potentially dropping functions marked as "pure" using pure comment annotation. See UglifyJS documentation for details.
86
+ :switches => true # de-duplicate and remove unreachable switch branches
87
+ }, # Apply transformations to code, set to false to skip
88
+ :parse => {
89
+ :bare_returns => false, # Allow top-level return statements.
90
+ :expression => false, # Parse a single expression, rather than a program (for parsing JSON).
91
+ :html5_comments => true, # Ignore HTML5 comments in input
92
+ :shebang => true, # support #!command as the first line
93
+ :strict => false
94
+ },
25
95
  :define => {}, # Define values for symbol replacement
26
- :beautify => false, # Ouput indented code
27
- :beautify_options => {
28
- :indent_level => 4,
29
- :indent_start => 0,
30
- :space_colon => false
31
- }
96
+ :keep_fnames => false, # Generate code safe for the poor souls relying on Function.prototype.name at run-time. Sets both compress and mangle keep_fanems to true.
97
+ :toplevel => false,
98
+ :ie8 => true, # Generate safe code for IE8
99
+ :source_map => false, # Generate source map
100
+ :error_context_lines => 8, # How many lines surrounding the error line
101
+ :harmony => false # Enable ES6/Harmony mode (experimental). Disabling mangling and compressing is recommended with Harmony mode.
32
102
  }
103
+ # rubocop:enable Layout/LineLength
33
104
 
34
- SourcePath = File.expand_path("../uglify.js", __FILE__)
35
- ES5FallbackPath = File.expand_path("../es5.js", __FILE__)
105
+ EXTRA_OPTIONS = [:comments, :mangle_properties]
106
+
107
+ MANGLE_PROPERTIES_DEFAULTS = {
108
+ :debug => false, # Add debug prefix and suffix to mangled properties
109
+ :regex => nil, # A regular expression to filter property names to be mangled
110
+ :keep_quoted => false, # Keep quoted property names
111
+ :reserved => [], # List of properties that should not be mangled
112
+ :builtins => false # Mangle properties that overlap with standard JS globals
113
+ }
114
+
115
+ SOURCE_MAP_DEFAULTS = {
116
+ :map_url => false, # Url for source mapping to be appended in minified source
117
+ :url => false, # Url for original source to be appended in minified source
118
+ :sources_content => false, # Include original source content in map
119
+ :filename => nil, # The filename of the input file
120
+ :root => nil, # The URL of the directory which contains :filename
121
+ :output_filename => nil, # The filename or URL where the minified output can be found
122
+ :input_source_map => nil # The contents of the source map describing the input
123
+ }
36
124
 
37
125
  # Minifies JavaScript code using implicit context.
38
126
  #
39
- # source should be a String or IO object containing valid JavaScript.
40
- # options contain optional overrides to Uglifier::DEFAULTS
41
- #
42
- # Returns minified code as String
127
+ # @param source [IO, String] valid JS source code.
128
+ # @param options [Hash] optional overrides to +Uglifier::DEFAULTS+
129
+ # @return [String] minified code.
43
130
  def self.compile(source, options = {})
44
- self.new(options).compile(source)
131
+ new(options).compile(source)
132
+ end
133
+
134
+ # Minifies JavaScript code and generates a source map using implicit context.
135
+ #
136
+ # @param source [IO, String] valid JS source code.
137
+ # @param options [Hash] optional overrides to +Uglifier::DEFAULTS+
138
+ # @return [Array(String, String)] minified code and source map.
139
+ def self.compile_with_map(source, options = {})
140
+ new(options).compile_with_map(source)
45
141
  end
46
142
 
47
143
  # Initialize new context for Uglifier with given options
48
144
  #
49
- # options - Hash of options to override Uglifier::DEFAULTS
145
+ # @param options [Hash] optional overrides to +Uglifier::DEFAULTS+
50
146
  def initialize(options = {})
51
- @options = DEFAULTS.merge(options)
52
- @context = ExecJS.compile(File.open(ES5FallbackPath, "r:UTF-8").read + File.open(SourcePath, "r:UTF-8").read)
147
+ # rubocop:disable Lint/UnreachableLoop
148
+ (options.keys - DEFAULTS.keys - EXTRA_OPTIONS)[0..1].each do |missing|
149
+ raise ArgumentError, "Invalid option: #{missing}"
150
+ end
151
+ # rubocop:enable Lint/UnreachableLoop
152
+ @options = options
53
153
  end
54
154
 
55
155
  # Minifies JavaScript code
56
156
  #
57
- # source should be a String or IO object containing valid JavaScript.
58
- #
59
- # Returns minified code as String
157
+ # @param source [IO, String] valid JS source code.
158
+ # @return [String] minified code.
60
159
  def compile(source)
61
- source = source.respond_to?(:read) ? source.read : source.to_s
160
+ if @options[:source_map]
161
+ compiled, source_map = run_uglifyjs(source, true)
162
+ source_map_uri = [source_map].pack('m0')
163
+ source_map_mime = "application/json;charset=utf-8;base64"
164
+ compiled + "\n//# sourceMappingURL=data:#{source_map_mime},#{source_map_uri}"
165
+ else
166
+ run_uglifyjs(source, false)
167
+ end
168
+ end
169
+ alias_method :compress, :compile
62
170
 
63
- js = []
64
- js << "var result = '';"
65
- js << "var source = #{json_encode(source)};"
66
- js << "var ast = UglifyJS.parser.parse(source);"
171
+ # Minifies JavaScript code and generates a source map
172
+ #
173
+ # @param source [IO, String] valid JS source code.
174
+ # @return [Array(String, String)] minified code and source map.
175
+ def compile_with_map(source)
176
+ run_uglifyjs(source, true)
177
+ end
67
178
 
68
- if @options[:lift_vars]
69
- js << "ast = UglifyJS.uglify.ast_lift_variables(ast);"
70
- end
179
+ private
71
180
 
72
- if @options[:copyright]
73
- js << <<-JS
74
- var comments = UglifyJS.parser.tokenizer(source)().comments_before;
75
- for (var i = 0; i < comments.length; i++) {
76
- var c = comments[i];
77
- result += (c.type == "comment1") ? "//"+c.value+"\\n" : "/*"+c.value+"*/\\n";
78
- }
79
- JS
181
+ def context
182
+ @context ||= begin
183
+ source = harmony? ? source_with(HarmonySourcePath) : source_with(SourcePath)
184
+ ExecJS.compile(source)
80
185
  end
186
+ end
81
187
 
82
- js << "ast = UglifyJS.uglify.ast_mangle(ast, #{json_encode(mangle_options)});"
188
+ def source_map_comments
189
+ return '' unless @options[:source_map].respond_to?(:[])
83
190
 
84
- if @options[:squeeze]
85
- js << "ast = UglifyJS.uglify.ast_squeeze(ast, #{json_encode(squeeze_options)});"
191
+ suffix = ''
192
+ if @options[:source_map][:map_url]
193
+ suffix += "\n//# sourceMappingURL=" + @options[:source_map][:map_url]
86
194
  end
87
195
 
88
- if @options[:unsafe]
89
- js << "ast = UglifyJS.uglify.ast_squeeze_more(ast);"
90
- end
196
+ suffix += "\n//# sourceURL=" + @options[:source_map][:url] if @options[:source_map][:url]
197
+ suffix
198
+ end
199
+
200
+ def source_with(path)
201
+ [ES5FallbackPath, SplitFallbackPath, SourceMapPath, path,
202
+ UglifyJSWrapperPath].map do |file|
203
+ File.open(file, "r:UTF-8", &:read)
204
+ end.join("\n")
205
+ end
91
206
 
92
- js << "result += UglifyJS.uglify.gen_code(ast, #{json_encode(gen_code_options)});"
207
+ # Run UglifyJS for given source code
208
+ def run_uglifyjs(input, generate_map)
209
+ source = read_source(input)
210
+ input_map = input_source_map(source, generate_map)
211
+ options = {
212
+ :source => source,
213
+ :output => output_options,
214
+ :compress => compressor_options,
215
+ :mangle => mangle_options,
216
+ :parse => parse_options,
217
+ :sourceMap => source_map_options(input_map),
218
+ :ie8 => ie8?
219
+ }
93
220
 
94
- if !@options[:beautify] && @options[:max_line_length]
95
- js << "result = UglifyJS.uglify.split_lines(result, #{@options[:max_line_length].to_i})"
96
- end
221
+ parse_result(context.call("uglifier", options), generate_map, options)
222
+ end
97
223
 
98
- js << "return result + ';';"
224
+ def harmony?
225
+ @options[:harmony]
226
+ end
99
227
 
100
- @context.exec js.join("\n")
228
+ def harmony_error_message(message)
229
+ if message.start_with?("Unexpected token")
230
+ ". To use ES6 syntax, harmony mode must be enabled with " \
231
+ "Uglifier.new(:harmony => true)."
232
+ else
233
+ ""
234
+ end
101
235
  end
102
- alias_method :compress, :compile
103
236
 
104
- private
237
+ def error_context_lines
238
+ @options.fetch(:error_context_lines, DEFAULTS[:error_context_lines]).to_i
239
+ end
105
240
 
106
- def mangle_options
241
+ def error_context_format_options(low, high, line_index, column)
242
+ line_width = high.to_s.size
107
243
  {
108
- "mangle" => @options[:mangle],
109
- "toplevel" => @options[:toplevel],
110
- "defines" => defines,
111
- "except" => @options[:except],
112
- "no_functions" => @options[:mangle] == :vars
244
+ :line_index => line_index,
245
+ :base_index => low,
246
+ :line_width => line_width,
247
+ :line_format => "\e[36m%#{line_width + 1}d\e[0m ", # cyan
248
+ :col => column
113
249
  }
114
250
  end
115
251
 
116
- def squeeze_options
117
- {
118
- "make_seqs" => @options[:seqs],
119
- "dead_code" => @options[:dead_code],
120
- "keep_comps" => !@options[:unsafe]
121
- }
252
+ def format_error_line(line, options)
253
+ # light red
254
+ indicator = ' => '.rjust(options[:line_width] + 2)
255
+ colored_line = "#{line[0...options[:col]]}\e[91m#{line[options[:col]..-1]}"
256
+ "\e[91m#{indicator}\e[0m#{colored_line}\e[0m"
122
257
  end
123
258
 
124
- def defines
125
- Hash[(@options[:define] || {}).map do |k, v|
126
- token = if v.is_a? Numeric
127
- ['num', v]
128
- elsif [true, false].include?(v)
129
- ['name', v.to_s]
130
- elsif v == nil
131
- ['name', 'null']
259
+ def format_lines(lines, options)
260
+ lines.map.with_index do |line, index|
261
+ if options[:base_index] + index == options[:line_index]
262
+ format_error_line(line, options)
132
263
  else
133
- ['string', v.to_s]
264
+ "#{options[:line_format] % (options[:base_index] + index + 1)}#{line}"
134
265
  end
135
- [k, token]
136
- end]
266
+ end
137
267
  end
138
268
 
139
- def gen_code_options
140
- options = {
141
- :ascii_only => @options[:ascii_only],
142
- :inline_script => @options[:inline_script],
143
- :quote_keys => @options[:quote_keys]
144
- }
269
+ def context_lines_message(source, line_number, column)
270
+ return if line_number.nil?
271
+
272
+ line_index = line_number - 1
273
+ lines = source.split("\n")
274
+
275
+ first_line = [line_index - error_context_lines, 0].max
276
+ last_line = [line_number + error_context_lines, lines.size].min
277
+ options = error_context_format_options(first_line, last_line, line_index, column)
278
+ context_lines = lines[first_line...last_line]
279
+
280
+ "--\n#{format_lines(context_lines, options).join("\n")}\n=="
281
+ end
282
+
283
+ def error_message(result, options)
284
+ err = result['error']
285
+ harmony_msg = harmony? ? '' : harmony_error_message(err['message'].to_s)
286
+ src_ctx = context_lines_message(options[:source], err['line'], err['col'])
287
+ "#{err['message']}#{harmony_msg}\n#{src_ctx}"
288
+ end
289
+
290
+ def parse_result(result, generate_map, options)
291
+ raise Error, error_message(result, options) if result.has_key?('error')
292
+
293
+ if generate_map
294
+ [result['code'] + source_map_comments, result['map']]
295
+ else
296
+ result['code'] + source_map_comments
297
+ end
298
+ end
299
+
300
+ def read_source(source)
301
+ if source.respond_to?(:read)
302
+ source.read
303
+ else
304
+ source.to_s
305
+ end
306
+ end
307
+
308
+ def mangle_options
309
+ defaults = conditional_option(
310
+ DEFAULTS[:mangle],
311
+ :keep_fnames => keep_fnames?(:mangle)
312
+ )
313
+
314
+ conditional_option(
315
+ @options[:mangle],
316
+ defaults,
317
+ :properties => mangle_properties_options
318
+ )
319
+ end
145
320
 
146
- if @options[:beautify]
147
- options.merge(:beautify => true).merge(@options[:beautify_options])
321
+ def mangle_properties_options
322
+ mangle_options = conditional_option(@options[:mangle], DEFAULTS[:mangle])
323
+
324
+ mangle_properties_options =
325
+ if @options.has_key?(:mangle_properties)
326
+ @options[:mangle_properties]
327
+ else
328
+ mangle_options && mangle_options[:properties]
329
+ end
330
+
331
+ options = conditional_option(mangle_properties_options, MANGLE_PROPERTIES_DEFAULTS)
332
+
333
+ if options && options[:regex]
334
+ options.merge(:regex => encode_regexp(options[:regex]))
335
+ else
336
+ options
337
+ end
338
+ end
339
+
340
+ def compressor_options
341
+ defaults = conditional_option(
342
+ DEFAULTS[:compress],
343
+ :global_defs => @options[:define] || {}
344
+ )
345
+
346
+ conditional_option(
347
+ @options[:compress],
348
+ defaults,
349
+ { :keep_fnames => keep_fnames?(:compress) }.merge(negate_iife_block)
350
+ )
351
+ end
352
+
353
+ # Prevent negate_iife when wrap_iife is true
354
+ def negate_iife_block
355
+ if output_options[:wrap_iife]
356
+ { :negate_iife => false }
357
+ else
358
+ {}
359
+ end
360
+ end
361
+
362
+ def comment_options
363
+ case comment_setting
364
+ when :all, true
365
+ true
366
+ when :jsdoc
367
+ "jsdoc"
368
+ when :copyright
369
+ encode_regexp(/(^!)|Copyright/i)
370
+ when Regexp
371
+ encode_regexp(comment_setting)
372
+ else
373
+ false
374
+ end
375
+ end
376
+
377
+ def quote_style
378
+ option = conditional_option(@options[:output], DEFAULTS[:output])[:quote_style]
379
+ case option
380
+ when :single
381
+ 1
382
+ when :double
383
+ 2
384
+ when :original
385
+ 3
386
+ when Numeric
387
+ option
388
+ else # auto
389
+ 0
390
+ end
391
+ end
392
+
393
+ def comment_setting
394
+ if @options.has_key?(:output) && @options[:output].has_key?(:comments)
395
+ @options[:output][:comments]
396
+ elsif @options.has_key?(:comments)
397
+ @options[:comments]
148
398
  else
399
+ DEFAULTS[:output][:comments]
400
+ end
401
+ end
402
+
403
+ def output_options
404
+ migrate_braces(DEFAULTS[:output].merge(@options[:output] || {}))
405
+ .merge(:comments => comment_options, :quote_style => quote_style)
406
+ end
407
+
408
+ def migrate_braces(options)
409
+ if harmony?
149
410
  options
411
+ else
412
+ options.merge(:braces => options[:bracketize]).delete_if { |key| key == :bracketize }
413
+ end
414
+ end
415
+
416
+ def ie8?
417
+ @options.fetch(:ie8, DEFAULTS[:ie8])
418
+ end
419
+
420
+ def keep_fnames?(type)
421
+ if @options[:keep_fnames] || DEFAULTS[:keep_fnames]
422
+ true
423
+ else
424
+ @options[type].respond_to?(:[]) && @options[type][:keep_fnames] ||
425
+ DEFAULTS[type].respond_to?(:[]) && DEFAULTS[type][:keep_fnames]
426
+ end
427
+ end
428
+
429
+ def source_map_options(input_map)
430
+ options = conditional_option(@options[:source_map], SOURCE_MAP_DEFAULTS) || SOURCE_MAP_DEFAULTS
431
+
432
+ {
433
+ :input => options[:filename],
434
+ :filename => options[:output_filename],
435
+ :root => options.fetch(:root) { input_map ? input_map["sourceRoot"] : nil },
436
+ :content => input_map,
437
+ #:map_url => options[:map_url],
438
+ :url => options[:url],
439
+ :includeSources => options[:sources_content]
440
+ }
441
+ end
442
+
443
+ def parse_options
444
+ conditional_option(@options[:parse], DEFAULTS[:parse])
445
+ .merge(parse_source_map_options)
446
+ end
447
+
448
+ def parse_source_map_options
449
+ if @options[:source_map].respond_to?(:[])
450
+ { :filename => @options[:source_map][:filename] }
451
+ else
452
+ {}
453
+ end
454
+ end
455
+
456
+ def enclose_options
457
+ if @options[:enclose]
458
+ @options[:enclose].map do |pair|
459
+ pair.first + ':' + pair.last
460
+ end
461
+ else
462
+ false
150
463
  end
151
464
  end
152
465
 
153
- # MultiJson API detection
154
- if MultiJson.respond_to? :dump
155
- def json_encode(obj)
156
- MultiJson.dump(obj)
466
+ def encode_regexp(regexp)
467
+ modifiers = if regexp.casefold?
468
+ "i"
469
+ else
470
+ ""
471
+ end
472
+
473
+ [regexp.source, modifiers]
474
+ end
475
+
476
+ def conditional_option(value, defaults, overrides = {})
477
+ if value == true || value.nil?
478
+ defaults.merge(overrides)
479
+ elsif value
480
+ defaults.merge(value).merge(overrides)
481
+ else
482
+ false
157
483
  end
158
- else
159
- def json_encode(obj)
160
- MultiJson.encode(obj)
484
+ end
485
+
486
+ def sanitize_map_root(map)
487
+ if map.nil?
488
+ nil
489
+ elsif map.is_a? String
490
+ sanitize_map_root(JSON.parse(map))
491
+ elsif map["sourceRoot"] == ""
492
+ map.merge("sourceRoot" => nil)
493
+ else
494
+ map
161
495
  end
162
496
  end
497
+
498
+ def extract_source_mapping_url(source)
499
+ comment_start = %r{(?://|/\*\s*)}
500
+ comment_end = %r{\s*(?:\r?\n?\*/|$)?}
501
+ source_mapping_regex = /#{comment_start}[@#]\ssourceMappingURL=\s*(\S*?)#{comment_end}/
502
+ rest = /\s#{comment_start}[@#]\s[a-zA-Z]+=\s*(?:\S*?)#{comment_end}/
503
+ regex = /#{source_mapping_regex}(?:#{rest})*\Z/m
504
+ match = regex.match(source)
505
+ match && match[1]
506
+ end
507
+
508
+ def input_source_map(source, generate_map)
509
+ return nil unless generate_map
510
+
511
+ source_map_options = @options[:source_map].is_a?(Hash) ? @options[:source_map] : {}
512
+ sanitize_map_root(source_map_options.fetch(:input_source_map) do
513
+ url = extract_source_mapping_url(source)
514
+ url.split(",", 2)[-1].unpack1('m0') if url && url.start_with?("data:")
515
+ end)
516
+ rescue ArgumentError, JSON::ParserError
517
+ nil
518
+ end
163
519
  end