uglifier 1.3.0 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/uglifier.rb CHANGED
@@ -1,163 +1,518 @@
1
1
  # encoding: UTF-8
2
2
 
3
+ require "json"
4
+ require "base64"
3
5
  require "execjs"
4
- require "multi_json"
6
+ require "uglifier/version"
5
7
 
8
+ # A wrapper around the UglifyJS interface
6
9
  class Uglifier
7
- Error = ExecJS::Error
8
- # MultiJson.engine = :json_gem
10
+ # Error class for compilation errors.
11
+ class Error < StandardError; end
12
+
13
+ # UglifyJS source path
14
+ SourcePath = File.expand_path("../uglify.js", __FILE__)
15
+ # UglifyJS with Harmony source path
16
+ HarmonySourcePath = File.expand_path("../uglify-harmony.js", __FILE__)
17
+ # Source Map path
18
+ SourceMapPath = File.expand_path("../source-map.js", __FILE__)
19
+ # ES5 shims source path
20
+ ES5FallbackPath = File.expand_path("../es5.js", __FILE__)
21
+ # String.split shim source path
22
+ SplitFallbackPath = File.expand_path("../split.js", __FILE__)
23
+ # UglifyJS wrapper path
24
+ UglifyJSWrapperPath = File.expand_path("../uglifier.js", __FILE__)
9
25
 
10
26
  # Default options for compilation
11
27
  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
28
+ # rubocop:disable LineLength
29
+ :output => {
30
+ :ascii_only => true, # Escape non-ASCII characterss
31
+ :comments => :copyright, # Preserve comments (:all, :jsdoc, :copyright, :none)
32
+ :inline_script => false, # Escape occurrences of </script in strings
33
+ :quote_keys => false, # Quote keys in object literals
34
+ :max_line_len => 32 * 1024, # Maximum line length in minified code
35
+ :bracketize => false, # Bracketize if, for, do, while or with statements, even if their body is a single statement
36
+ :semicolons => true, # Separate statements with semicolons
37
+ :preserve_line => false, # Preserve line numbers in outputs
38
+ :beautify => false, # Beautify output
39
+ :indent_level => 4, # Indent level in spaces
40
+ :indent_start => 0, # Starting indent level
41
+ :width => 80, # Specify line width when beautifier is used (only with beautifier)
42
+ :preamble => nil, # Preamble for the generated JS file. Can be used to insert any code or comment.
43
+ :wrap_iife => false, # Wrap IIFEs in parenthesis. Note: this disables the negate_iife compression option.
44
+ :shebang => true, # Preserve shebang (#!) in preamble (shell scripts)
45
+ :quote_style => 0, # Quote style, possible values :auto (default), :single, :double, :original
46
+ :keep_quoted_props => false # Keep quotes property names
47
+ },
48
+ :mangle => {
49
+ :eval => false, # Mangle names when eval of when is used in scope
50
+ :reserved => ["$super"], # Argument names to be excluded from mangling
51
+ :properties => false, # Mangle property names
52
+ :toplevel => false, # Mangle names declared in the toplevel scope
53
+ }, # Mangle variable and function names, set to false to skip mangling
54
+ :compress => {
55
+ :sequences => true, # Allow statements to be joined by commas
56
+ :properties => true, # Rewrite property access using the dot notation
57
+ :dead_code => true, # Remove unreachable code
58
+ :drop_debugger => true, # Remove debugger; statements
59
+ :unsafe => false, # Apply "unsafe" transformations
60
+ :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.
61
+ :unsafe_math => false, # Optimize numerical expressions like 2 * x * 3 into 6 * x, which may give imprecise floating point results.
62
+ :unsafe_proto => false, # Optimize expressions like Array.prototype.slice.call(a) into [].slice.call(a)
63
+ :conditionals => true, # Optimize for if-s and conditional expressions
64
+ :comparisons => true, # Apply binary node optimizations for comparisons
65
+ :evaluate => true, # Attempt to evaluate constant expressions
66
+ :booleans => true, # Various optimizations to boolean contexts
67
+ :loops => true, # Optimize loops when condition can be statically determined
68
+ :unused => true, # Drop unreferenced functions and variables (simple direct variable assignments do not count as references unless set to `"keep_assign"`)
69
+ :toplevel => false, # Drop unreferenced top-level functions and variables
70
+ :top_retain => [], # prevent specific toplevel functions and variables from `unused` removal
71
+ :hoist_funs => true, # Hoist function declarations
72
+ :hoist_vars => false, # Hoist var declarations
73
+ :if_return => true, # Optimizations for if/return and if/continue
74
+ :join_vars => true, # Join consecutive var statements
75
+ :collapse_vars => true, # Collapse single-use var and const definitions when possible.
76
+ :reduce_funcs => false, # Inline single-use functions as function expressions. Depends on reduce_vars.
77
+ :reduce_vars => false, # Collapse variables assigned with and used as constant values.
78
+ :negate_iife => true, # Negate immediately invoked function expressions to avoid extra parens
79
+ :pure_getters => false, # Assume that object property access does not have any side-effects
80
+ :pure_funcs => nil, # List of functions without side-effects. Can safely discard function calls when the result value is not used
81
+ :drop_console => false, # Drop calls to console.* functions
82
+ :keep_fargs => false, # Preserve unused function arguments
83
+ :keep_fnames => false, # Do not drop names in function definitions
84
+ :passes => 1, # Number of times to run compress. Raising the number of passes will increase compress time, but can produce slightly smaller code.
85
+ :keep_infinity => false, # Prevent compression of Infinity to 1/0
86
+ :side_effects => true, # Pass false to disable potentially dropping functions marked as "pure" using pure comment annotation. See UglifyJS documentation for details.
87
+ :switches => true, # de-duplicate and remove unreachable switch branches
88
+ }, # Apply transformations to code, set to false to skip
89
+ :parse => {
90
+ :bare_returns => false, # Allow top-level return statements.
91
+ :expression => false, # Parse a single expression, rather than a program (for parsing JSON).
92
+ :html5_comments => true, # Ignore HTML5 comments in input
93
+ :shebang => true, # support #!command as the first line
94
+ :strict => false
95
+ },
25
96
  :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
- }
97
+ :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.
98
+ :toplevel => false,
99
+ :ie8 => true, # Generate safe code for IE8
100
+ :source_map => false, # Generate source map
101
+ :error_context_lines => 8, # How many lines surrounding the error line
102
+ :harmony => false # Enable ES6/Harmony mode (experimental). Disabling mangling and compressing is recommended with Harmony mode.
32
103
  }
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
+ }
124
+
125
+ # rubocop:enable LineLength
36
126
 
37
127
  # Minifies JavaScript code using implicit context.
38
128
  #
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
129
+ # @param source [IO, String] valid JS source code.
130
+ # @param options [Hash] optional overrides to +Uglifier::DEFAULTS+
131
+ # @return [String] minified code.
43
132
  def self.compile(source, options = {})
44
- self.new(options).compile(source)
133
+ new(options).compile(source)
134
+ end
135
+
136
+ # Minifies JavaScript code and generates a source map using implicit context.
137
+ #
138
+ # @param source [IO, String] valid JS source code.
139
+ # @param options [Hash] optional overrides to +Uglifier::DEFAULTS+
140
+ # @return [Array(String, String)] minified code and source map.
141
+ def self.compile_with_map(source, options = {})
142
+ new(options).compile_with_map(source)
45
143
  end
46
144
 
47
145
  # Initialize new context for Uglifier with given options
48
146
  #
49
- # options - Hash of options to override Uglifier::DEFAULTS
147
+ # @param options [Hash] optional overrides to +Uglifier::DEFAULTS+
50
148
  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)
149
+ (options.keys - DEFAULTS.keys - EXTRA_OPTIONS)[0..1].each do |missing|
150
+ raise ArgumentError, "Invalid option: #{missing}"
151
+ end
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 = Base64.strict_encode64(source_map)
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
91
199
 
92
- js << "result += UglifyJS.uglify.gen_code(ast, #{json_encode(gen_code_options)});"
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
93
206
 
94
- if !@options[:beautify] && @options[:max_line_length]
95
- js << "result = UglifyJS.uglify.split_lines(result, #{@options[:max_line_length].to_i})"
96
- end
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
+ }
97
220
 
98
- js << "return result + ';';"
221
+ parse_result(context.call("uglifier", options), generate_map, options)
222
+ end
99
223
 
100
- @context.exec js.join("\n")
224
+ def harmony?
225
+ @options[:harmony]
101
226
  end
102
- alias_method :compress, :compile
103
227
 
104
- private
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
235
+ end
105
236
 
106
- def mangle_options
107
- {
108
- "mangle" => @options[:mangle],
109
- "toplevel" => @options[:toplevel],
110
- "defines" => defines,
111
- "except" => @options[:except],
112
- "no_functions" => @options[:mangle] == :vars
113
- }
237
+ def error_context_lines
238
+ @options.fetch(:error_context_lines, DEFAULTS[:error_context_lines]).to_i
114
239
  end
115
240
 
116
- def squeeze_options
241
+ def error_context_format_options(low, high, line_index, column)
242
+ line_width = high.to_s.size
117
243
  {
118
- "make_seqs" => @options[:seqs],
119
- "dead_code" => @options[:dead_code],
120
- "keep_comps" => !@options[:unsafe]
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
121
249
  }
122
250
  end
123
251
 
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']
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"
257
+ end
258
+
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
320
+
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
145
392
 
146
- if @options[:beautify]
147
- options.merge(:beautify => true).merge(@options[:beautify_options])
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
+ {}
150
453
  end
151
454
  end
152
455
 
153
- # MultiJson API detection
154
- if MultiJson.respond_to? :dump
155
- def json_encode(obj)
156
- MultiJson.dump(obj)
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
463
+ end
464
+ end
465
+
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
+ source_map_options = @options[:source_map].is_a?(Hash) ? @options[:source_map] : {}
511
+ sanitize_map_root(source_map_options.fetch(:input_source_map) do
512
+ url = extract_source_mapping_url(source)
513
+ Base64.strict_decode64(url.split(",", 2)[-1]) if url && url.start_with?("data:")
514
+ end)
515
+ rescue ArgumentError, JSON::ParserError
516
+ nil
517
+ end
163
518
  end