Browse Source

Merge remote-tracking branch 'upstream/master'

Sander Snel 2 weeks ago
parent
commit
0f6d616b85

+ 3
- 2
Gemfile View File

@@ -111,7 +111,7 @@ group :production, :test do
111 111
 end
112 112
 
113 113
 group :test do
114
-  gem 'capybara', '~> 3.24'
114
+  gem 'capybara', '~> 3.25'
115 115
   gem 'climate_control', '~> 0.2'
116 116
   gem 'faker', '~> 1.9'
117 117
   gem 'microformats', '~> 4.1'
@@ -131,7 +131,7 @@ group :development do
131 131
   gem 'letter_opener', '~> 1.7'
132 132
   gem 'letter_opener_web', '~> 1.3'
133 133
   gem 'memory_profiler'
134
-  gem 'rubocop', '~> 0.71', require: false
134
+  gem 'rubocop', '~> 0.72', require: false
135 135
   gem 'rubocop-rails', '~> 2.0', require: false
136 136
   gem 'brakeman', '~> 4.5', require: false
137 137
   gem 'bundler-audit', '~> 0.6', require: false
@@ -151,3 +151,4 @@ group :production do
151 151
 end
152 152
 
153 153
 gem 'concurrent-ruby', require: false
154
+gem 'connection_pool', require: false

+ 9
- 8
Gemfile.lock View File

@@ -106,7 +106,7 @@ GEM
106 106
     brakeman (4.5.1)
107 107
     browser (2.5.3)
108 108
     builder (3.2.3)
109
-    bullet (6.0.0)
109
+    bullet (6.0.1)
110 110
       activesupport (>= 3.0.0)
111 111
       uniform_notifier (~> 1.11)
112 112
     bundler-audit (0.6.1)
@@ -129,7 +129,7 @@ GEM
129 129
       sshkit (~> 1.3)
130 130
     capistrano-yarn (2.0.2)
131 131
       capistrano (~> 3.0)
132
-    capybara (3.24.0)
132
+    capybara (3.25.0)
133 133
       addressable
134 134
       mini_mime (>= 0.1.3)
135 135
       nokogiri (~> 1.8)
@@ -289,7 +289,7 @@ GEM
289 289
     idn-ruby (0.1.0)
290 290
     ipaddress (0.8.3)
291 291
     iso-639 (0.2.8)
292
-    jaro_winkler (1.5.2)
292
+    jaro_winkler (1.5.3)
293 293
     jmespath (1.4.0)
294 294
     json (2.1.0)
295 295
     json-ld (3.0.2)
@@ -338,7 +338,7 @@ GEM
338 338
       mimemagic (~> 0.3.2)
339 339
     mario-redis-lock (1.2.1)
340 340
       redis (>= 3.0.5)
341
-    memory_profiler (0.9.13)
341
+    memory_profiler (0.9.14)
342 342
     method_source (0.9.2)
343 343
     microformats (4.1.0)
344 344
       json (~> 2.1)
@@ -422,7 +422,7 @@ GEM
422 422
       pry (~> 0.10)
423 423
     pry-rails (0.3.9)
424 424
       pry (>= 0.10.4)
425
-    public_suffix (3.1.0)
425
+    public_suffix (3.1.1)
426 426
     puma (3.12.1)
427 427
     pundit (2.0.1)
428 428
       activesupport (>= 3.0.0)
@@ -527,7 +527,7 @@ GEM
527 527
       rspec-core (~> 3.0, >= 3.0.0)
528 528
       sidekiq (>= 2.4.0)
529 529
     rspec-support (3.8.0)
530
-    rubocop (0.71.0)
530
+    rubocop (0.72.0)
531 531
       jaro_winkler (~> 1.5.1)
532 532
       parallel (~> 1.10)
533 533
       parser (>= 2.6)
@@ -663,12 +663,13 @@ DEPENDENCIES
663 663
   capistrano-rails (~> 1.4)
664 664
   capistrano-rbenv (~> 2.1)
665 665
   capistrano-yarn (~> 2.0)
666
-  capybara (~> 3.24)
666
+  capybara (~> 3.25)
667 667
   charlock_holmes (~> 0.7.6)
668 668
   chewy (~> 5.0)
669 669
   cld3 (~> 3.2.4)
670 670
   climate_control (~> 0.2)
671 671
   concurrent-ruby
672
+  connection_pool
672 673
   derailed_benchmarks
673 674
   devise (~> 4.6)
674 675
   devise-two-factor (~> 3.0)
@@ -742,7 +743,7 @@ DEPENDENCIES
742 743
   rqrcode (~> 0.10)
743 744
   rspec-rails (~> 3.8)
744 745
   rspec-sidekiq (~> 3.0)
745
-  rubocop (~> 0.71)
746
+  rubocop (~> 0.72)
746 747
   rubocop-rails (~> 2.0)
747 748
   sanitize (~> 5.0)
748 749
   sidekiq (~> 5.2)

+ 2
- 12
app/controllers/api/v1/statuses_controller.rb View File

@@ -5,8 +5,8 @@ class Api::V1::StatusesController < Api::BaseController
5 5
 
6 6
   before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
7 7
   before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only:   [:create, :destroy]
8
-  before_action :require_user!, except:  [:show, :context, :card]
9
-  before_action :set_status, only:       [:show, :context, :card]
8
+  before_action :require_user!, except:  [:show, :context]
9
+  before_action :set_status, only:       [:show, :context]
10 10
 
11 11
   respond_to :json
12 12
 
@@ -33,16 +33,6 @@ class Api::V1::StatusesController < Api::BaseController
33 33
     render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
34 34
   end
35 35
 
36
-  def card
37
-    @card = @status.preview_cards.first
38
-
39
-    if @card.nil?
40
-      render_empty
41
-    else
42
-      render json: @card, serializer: REST::PreviewCardSerializer
43
-    end
44
-  end
45
-
46 36
   def create
47 37
     @status = PostStatusService.new.call(current_user.account,
48 38
                                          text: status_params[:status],

+ 4
- 1
app/javascript/flavours/glitch/features/compose/components/compose_form.js View File

@@ -197,7 +197,10 @@ class ComposeForm extends ImmutablePureComponent {
197 197
 
198 198
   handleFocus = () => {
199 199
     if (this.composeForm && !this.props.singleColumn) {
200
-      this.composeForm.scrollIntoView();
200
+      const { left, right } = this.composeForm.getBoundingClientRect();
201
+      if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) {
202
+        this.composeForm.scrollIntoView();
203
+      }
201 204
     }
202 205
   }
203 206
 

+ 44
- 24
app/javascript/flavours/glitch/features/status/index.js View File

@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
4 4
 import PropTypes from 'prop-types';
5 5
 import classNames from 'classnames';
6 6
 import ImmutablePropTypes from 'react-immutable-proptypes';
7
+import { createSelector } from 'reselect';
7 8
 import { fetchStatus } from 'flavours/glitch/actions/statuses';
8 9
 import MissingIndicator from 'flavours/glitch/components/missing_indicator';
9 10
 import DetailedStatus from './components/detailed_status';
@@ -61,39 +62,58 @@ const messages = defineMessages({
61 62
 const makeMapStateToProps = () => {
62 63
   const getStatus = makeGetStatus();
63 64
 
64
-  const mapStateToProps = (state, props) => {
65
-    const status = getStatus(state, { id: props.params.statusId });
65
+  const getAncestorsIds = createSelector([
66
+    (_, { id }) => id,
67
+    state => state.getIn(['contexts', 'inReplyTos']),
68
+  ], (statusId, inReplyTos) => {
66 69
     let ancestorsIds = Immutable.List();
70
+    ancestorsIds = ancestorsIds.withMutations(mutable => {
71
+      let id = statusId;
72
+
73
+      while (id) {
74
+        mutable.unshift(id);
75
+        id = inReplyTos.get(id);
76
+      }
77
+    });
78
+
79
+    return ancestorsIds;
80
+  });
81
+
82
+  const getDescendantsIds = createSelector([
83
+    (_, { id }) => id,
84
+    state => state.getIn(['contexts', 'replies']),
85
+  ], (statusId, contextReplies) => {
67 86
     let descendantsIds = Immutable.List();
87
+    descendantsIds = descendantsIds.withMutations(mutable => {
88
+      const ids = [statusId];
68 89
 
69
-    if (status) {
70
-      ancestorsIds = ancestorsIds.withMutations(mutable => {
71
-        let id = status.get('in_reply_to_id');
90
+      while (ids.length > 0) {
91
+        let id        = ids.shift();
92
+        const replies = contextReplies.get(id);
72 93
 
73
-        while (id) {
74
-          mutable.unshift(id);
75
-          id = state.getIn(['contexts', 'inReplyTos', id]);
94
+        if (statusId !== id) {
95
+          mutable.push(id);
76 96
         }
77
-      });
78 97
 
79
-      descendantsIds = descendantsIds.withMutations(mutable => {
80
-        const ids = [status.get('id')];
98
+        if (replies) {
99
+          replies.reverse().forEach(reply => {
100
+            ids.unshift(reply);
101
+          });
102
+        }
103
+      }
104
+    });
81 105
 
82
-        while (ids.length > 0) {
83
-          let id        = ids.shift();
84
-          const replies = state.getIn(['contexts', 'replies', id]);
106
+    return descendantsIds;
107
+  });
85 108
 
86
-          if (status.get('id') !== id) {
87
-            mutable.push(id);
88
-          }
109
+  const mapStateToProps = (state, props) => {
110
+    const status = getStatus(state, { id: props.params.statusId });
111
+    let ancestorsIds = Immutable.List();
112
+    let descendantsIds = Immutable.List();
89 113
 
90
-          if (replies) {
91
-            replies.reverse().forEach(reply => {
92
-              ids.unshift(reply);
93
-            });
94
-          }
95
-        }
96
-      });
114
+    if (status) {
115
+      ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
116
+      descendantsIds = getDescendantsIds(state, { id: status.get('id') });
97 117
     }
98 118
 
99 119
     return {

+ 7
- 1
app/javascript/flavours/glitch/reducers/compose.js View File

@@ -275,6 +275,12 @@ const expandMentions = status => {
275 275
   return fragment.innerHTML;
276 276
 };
277 277
 
278
+const expiresInFromExpiresAt = expires_at => {
279
+  if (!expires_at) return 24 * 3600;
280
+  const delta = (new Date(expires_at).getTime() - Date.now()) / 1000;
281
+  return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600;
282
+};
283
+
278 284
 export default function compose(state = initialState, action) {
279 285
   switch(action.type) {
280 286
   case STORE_HYDRATE:
@@ -456,7 +462,7 @@ export default function compose(state = initialState, action) {
456 462
         map.set('poll', ImmutableMap({
457 463
           options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
458 464
           multiple: action.status.getIn(['poll', 'multiple']),
459
-          expires_in: 24 * 3600,
465
+          expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])),
460 466
         }));
461 467
       }
462 468
     });

+ 4
- 1
app/javascript/mastodon/features/compose/components/compose_form.js View File

@@ -118,7 +118,10 @@ class ComposeForm extends ImmutablePureComponent {
118 118
 
119 119
   handleFocus = () => {
120 120
     if (this.composeForm && !this.props.singleColumn) {
121
-      this.composeForm.scrollIntoView();
121
+      const { left, right } = this.composeForm.getBoundingClientRect();
122
+      if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) {
123
+        this.composeForm.scrollIntoView();
124
+      }
122 125
     }
123 126
   }
124 127
 

+ 44
- 24
app/javascript/mastodon/features/status/index.js View File

@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
4 4
 import PropTypes from 'prop-types';
5 5
 import classNames from 'classnames';
6 6
 import ImmutablePropTypes from 'react-immutable-proptypes';
7
+import { createSelector } from 'reselect';
7 8
 import { fetchStatus } from '../../actions/statuses';
8 9
 import MissingIndicator from '../../components/missing_indicator';
9 10
 import DetailedStatus from './components/detailed_status';
@@ -63,39 +64,58 @@ const messages = defineMessages({
63 64
 const makeMapStateToProps = () => {
64 65
   const getStatus = makeGetStatus();
65 66
 
66
-  const mapStateToProps = (state, props) => {
67
-    const status = getStatus(state, { id: props.params.statusId });
67
+  const getAncestorsIds = createSelector([
68
+    (_, { id }) => id,
69
+    state => state.getIn(['contexts', 'inReplyTos']),
70
+  ], (statusId, inReplyTos) => {
68 71
     let ancestorsIds = Immutable.List();
72
+    ancestorsIds = ancestorsIds.withMutations(mutable => {
73
+      let id = statusId;
74
+
75
+      while (id) {
76
+        mutable.unshift(id);
77
+        id = inReplyTos.get(id);
78
+      }
79
+    });
80
+
81
+    return ancestorsIds;
82
+  });
83
+
84
+  const getDescendantsIds = createSelector([
85
+    (_, { id }) => id,
86
+    state => state.getIn(['contexts', 'replies']),
87
+  ], (statusId, contextReplies) => {
69 88
     let descendantsIds = Immutable.List();
89
+    descendantsIds = descendantsIds.withMutations(mutable => {
90
+      const ids = [statusId];
70 91
 
71
-    if (status) {
72
-      ancestorsIds = ancestorsIds.withMutations(mutable => {
73
-        let id = status.get('in_reply_to_id');
92
+      while (ids.length > 0) {
93
+        let id        = ids.shift();
94
+        const replies = contextReplies.get(id);
74 95
 
75
-        while (id) {
76
-          mutable.unshift(id);
77
-          id = state.getIn(['contexts', 'inReplyTos', id]);
96
+        if (statusId !== id) {
97
+          mutable.push(id);
78 98
         }
79
-      });
80 99
 
81
-      descendantsIds = descendantsIds.withMutations(mutable => {
82
-        const ids = [status.get('id')];
100
+        if (replies) {
101
+          replies.reverse().forEach(reply => {
102
+            ids.unshift(reply);
103
+          });
104
+        }
105
+      }
106
+    });
83 107
 
84
-        while (ids.length > 0) {
85
-          let id        = ids.shift();
86
-          const replies = state.getIn(['contexts', 'replies', id]);
108
+    return descendantsIds;
109
+  });
87 110
 
88
-          if (status.get('id') !== id) {
89
-            mutable.push(id);
90
-          }
111
+  const mapStateToProps = (state, props) => {
112
+    const status = getStatus(state, { id: props.params.statusId });
113
+    let ancestorsIds = Immutable.List();
114
+    let descendantsIds = Immutable.List();
91 115
 
92
-          if (replies) {
93
-            replies.reverse().forEach(reply => {
94
-              ids.unshift(reply);
95
-            });
96
-          }
97
-        }
98
-      });
116
+    if (status) {
117
+      ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
118
+      descendantsIds = getDescendantsIds(state, { id: status.get('id') });
99 119
     }
100 120
 
101 121
     return {

+ 7
- 1
app/javascript/mastodon/reducers/compose.js View File

@@ -195,6 +195,12 @@ const expandMentions = status => {
195 195
   return fragment.innerHTML;
196 196
 };
197 197
 
198
+const expiresInFromExpiresAt = expires_at => {
199
+  if (!expires_at) return 24 * 3600;
200
+  const delta = (new Date(expires_at).getTime() - Date.now()) / 1000;
201
+  return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600;
202
+};
203
+
198 204
 export default function compose(state = initialState, action) {
199 205
   switch(action.type) {
200 206
   case STORE_HYDRATE:
@@ -353,7 +359,7 @@ export default function compose(state = initialState, action) {
353 359
         map.set('poll', ImmutableMap({
354 360
           options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
355 361
           multiple: action.status.getIn(['poll', 'multiple']),
356
-          expires_in: 24 * 3600,
362
+          expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])),
357 363
         }));
358 364
       }
359 365
     });

+ 63
- 0
app/lib/connection_pool/shared_connection_pool.rb View File

@@ -0,0 +1,63 @@
1
+# frozen_string_literal: true
2
+
3
+require 'connection_pool'
4
+require_relative './shared_timed_stack'
5
+
6
+class ConnectionPool::SharedConnectionPool < ConnectionPool
7
+  def initialize(options = {}, &block)
8
+    super(options, &block)
9
+
10
+    @available = ConnectionPool::SharedTimedStack.new(@size, &block)
11
+  end
12
+
13
+  delegate :size, :flush, to: :@available
14
+
15
+  def with(preferred_tag, options = {})
16
+    Thread.handle_interrupt(Exception => :never) do
17
+      conn = checkout(preferred_tag, options)
18
+
19
+      begin
20
+        Thread.handle_interrupt(Exception => :immediate) do
21
+          yield conn
22
+        end
23
+      ensure
24
+        checkin(preferred_tag)
25
+      end
26
+    end
27
+  end
28
+
29
+  def checkout(preferred_tag, options = {})
30
+    if ::Thread.current[key(preferred_tag)]
31
+      ::Thread.current[key_count(preferred_tag)] += 1
32
+      ::Thread.current[key(preferred_tag)]
33
+    else
34
+      ::Thread.current[key_count(preferred_tag)] = 1
35
+      ::Thread.current[key(preferred_tag)] = @available.pop(preferred_tag, options[:timeout] || @timeout)
36
+    end
37
+  end
38
+
39
+  def checkin(preferred_tag)
40
+    if ::Thread.current[key(preferred_tag)]
41
+      if ::Thread.current[key_count(preferred_tag)] == 1
42
+        @available.push(::Thread.current[key(preferred_tag)])
43
+        ::Thread.current[key(preferred_tag)] = nil
44
+      else
45
+        ::Thread.current[key_count(preferred_tag)] -= 1
46
+      end
47
+    else
48
+      raise ConnectionPool::Error, 'no connections are checked out'
49
+    end
50
+
51
+    nil
52
+  end
53
+
54
+  private
55
+
56
+  def key(tag)
57
+    :"#{@key}-#{tag}"
58
+  end
59
+
60
+  def key_count(tag)
61
+    :"#{@key_count}-#{tag}"
62
+  end
63
+end

+ 95
- 0
app/lib/connection_pool/shared_timed_stack.rb View File

@@ -0,0 +1,95 @@
1
+# frozen_string_literal: true
2
+
3
+class ConnectionPool::SharedTimedStack
4
+  def initialize(max = 0, &block)
5
+    @create_block = block
6
+    @max          = max
7
+    @created      = 0
8
+    @queue        = []
9
+    @tagged_queue = Hash.new { |hash, key| hash[key] = [] }
10
+    @mutex        = Mutex.new
11
+    @resource     = ConditionVariable.new
12
+  end
13
+
14
+  def push(connection)
15
+    @mutex.synchronize do
16
+      store_connection(connection)
17
+      @resource.broadcast
18
+    end
19
+  end
20
+
21
+  alias << push
22
+
23
+  def pop(preferred_tag, timeout = 5.0)
24
+    deadline = current_time + timeout
25
+
26
+    @mutex.synchronize do
27
+      loop do
28
+        return fetch_preferred_connection(preferred_tag) unless @tagged_queue[preferred_tag].empty?
29
+
30
+        connection = try_create(preferred_tag)
31
+        return connection if connection
32
+
33
+        to_wait = deadline - current_time
34
+        raise Timeout::Error, "Waited #{timeout} sec" if to_wait <= 0
35
+
36
+        @resource.wait(@mutex, to_wait)
37
+      end
38
+    end
39
+  end
40
+
41
+  def empty?
42
+    size.zero?
43
+  end
44
+
45
+  def size
46
+    @mutex.synchronize do
47
+      @queue.size
48
+    end
49
+  end
50
+
51
+  def flush
52
+    @mutex.synchronize do
53
+      @queue.delete_if do |connection|
54
+        delete = !connection.in_use && (connection.dead || connection.seconds_idle >= RequestPool::MAX_IDLE_TIME)
55
+
56
+        if delete
57
+          @tagged_queue[connection.site].delete(connection)
58
+          connection.close
59
+          @created -= 1
60
+        end
61
+
62
+        delete
63
+      end
64
+    end
65
+  end
66
+
67
+  private
68
+
69
+  def try_create(preferred_tag)
70
+    if @created == @max && !@queue.empty?
71
+      throw_away_connection = @queue.pop
72
+      @tagged_queue[throw_away_connection.site].delete(throw_away_connection)
73
+      @create_block.call(preferred_tag)
74
+    elsif @created != @max
75
+      connection = @create_block.call(preferred_tag)
76
+      @created += 1
77
+      connection
78
+    end
79
+  end
80
+
81
+  def fetch_preferred_connection(preferred_tag)
82
+    connection = @tagged_queue[preferred_tag].pop
83
+    @queue.delete(connection)
84
+    connection
85
+  end
86
+
87
+  def current_time
88
+    Process.clock_gettime(Process::CLOCK_MONOTONIC)
89
+  end
90
+
91
+  def store_connection(connection)
92
+    @tagged_queue[connection.site].push(connection)
93
+    @queue.push(connection)
94
+  end
95
+end

+ 77
- 32
app/lib/request.rb View File

@@ -17,15 +17,22 @@ end
17 17
 class Request
18 18
   REQUEST_TARGET = '(request-target)'
19 19
 
20
+  # We enforce a 5s timeout on DNS resolving, 5s timeout on socket opening
21
+  # and 5s timeout on the TLS handshake, meaning the worst case should take
22
+  # about 15s in total
23
+  TIMEOUT = { connect: 5, read: 10, write: 10 }.freeze
24
+
20 25
   include RoutingHelper
21 26
 
22 27
   def initialize(verb, url, **options)
23 28
     raise ArgumentError if url.blank?
24 29
 
25
-    @verb    = verb
26
-    @url     = Addressable::URI.parse(url).normalize
27
-    @options = options.merge(use_proxy? ? Rails.configuration.x.http_client_proxy : { socket_class: Socket })
28
-    @headers = {}
30
+    @verb        = verb
31
+    @url         = Addressable::URI.parse(url).normalize
32
+    @http_client = options.delete(:http_client)
33
+    @options     = options.merge(socket_class: use_proxy? ? ProxySocket : Socket)
34
+    @options     = @options.merge(Rails.configuration.x.http_client_proxy) if use_proxy?
35
+    @headers     = {}
29 36
 
30 37
     raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service?
31 38
 
@@ -50,15 +57,24 @@ class Request
50 57
 
51 58
   def perform
52 59
     begin
53
-      response = http_client.headers(headers).public_send(@verb, @url.to_s, @options)
60
+      response = http_client.public_send(@verb, @url.to_s, @options.merge(headers: headers))
54 61
     rescue => e
55 62
       raise e.class, "#{e.message} on #{@url}", e.backtrace[0]
56 63
     end
57 64
 
58 65
     begin
59
-      yield response.extend(ClientLimit) if block_given?
66
+      response = response.extend(ClientLimit)
67
+
68
+      # If we are using a persistent connection, we have to
69
+      # read every response to be able to move forward at all.
70
+      # However, simply calling #to_s or #flush may not be safe,
71
+      # as the response body, if malicious, could be too big
72
+      # for our memory. So we use the #body_with_limit method
73
+      response.body_with_limit if http_client.persistent?
74
+
75
+      yield response if block_given?
60 76
     ensure
61
-      http_client.close
77
+      http_client.close unless http_client.persistent?
62 78
     end
63 79
   end
64 80
 
@@ -76,6 +92,10 @@ class Request
76 92
 
77 93
       %w(http https).include?(parsed_url.scheme) && parsed_url.host.present?
78 94
     end
95
+
96
+    def http_client
97
+      HTTP.use(:auto_inflate).timeout(:per_operation, TIMEOUT.dup).follow(max_hops: 2)
98
+    end
79 99
   end
80 100
 
81 101
   private
@@ -116,16 +136,8 @@ class Request
116 136
     end
117 137
   end
118 138
 
119
-  def timeout
120
-    # We enforce a 1s timeout on DNS resolving, 10s timeout on socket opening
121
-    # and 5s timeout on the TLS handshake, meaning the worst case should take
122
-    # about 16s in total
123
-
124
-    { connect: 5, read: 10, write: 10 }
125
-  end
126
-
127 139
   def http_client
128
-    @http_client ||= HTTP.use(:auto_inflate).timeout(:per_operation, timeout).follow(max_hops: 2)
140
+    @http_client ||= Request.http_client
129 141
   end
130 142
 
131 143
   def use_proxy?
@@ -166,26 +178,49 @@ class Request
166 178
   class Socket < TCPSocket
167 179
     class << self
168 180
       def open(host, *args)
169
-        return super(host, *args) if thru_hidden_service?(host)
170
-
171 181
         outer_e = nil
182
+        port    = args.first
172 183
 
173
-        Resolv::DNS.open do |dns|
174
-          dns.timeouts = 5
184
+        addresses = []
185
+        begin
186
+          addresses = [IPAddr.new(host)]
187
+        rescue IPAddr::InvalidAddressError
188
+          Resolv::DNS.open do |dns|
189
+            dns.timeouts = 5
190
+            addresses = dns.getaddresses(host).take(2)
191
+          end
192
+        end
175 193
 
176
-          addresses = dns.getaddresses(host).take(2)
177
-          time_slot = 10.0 / addresses.size
194
+        addresses.each do |address|
195
+          begin
196
+            check_private_address(address)
178 197
 
179
-          addresses.each do |address|
180
-            begin
181
-              raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(IPAddr.new(address.to_s))
198
+            sock     = ::Socket.new(address.is_a?(Resolv::IPv6) ? ::Socket::AF_INET6 : ::Socket::AF_INET, ::Socket::SOCK_STREAM, 0)
199
+            sockaddr = ::Socket.pack_sockaddr_in(port, address.to_s)
200
+
201
+            sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
182 202
 
183
-              ::Timeout.timeout(time_slot, HTTP::TimeoutError) do
184
-                return super(address.to_s, *args)
203
+            begin
204
+              sock.connect_nonblock(sockaddr)
205
+            rescue IO::WaitWritable
206
+              if IO.select(nil, [sock], nil, Request::TIMEOUT[:connect])
207
+                begin
208
+                  sock.connect_nonblock(sockaddr)
209
+                rescue Errno::EISCONN
210
+                  # Yippee!
211
+                rescue
212
+                  sock.close
213
+                  raise
214
+                end
215
+              else
216
+                sock.close
217
+                raise HTTP::TimeoutError, "Connect timed out after #{Request::TIMEOUT[:connect]} seconds"
185 218
               end
186
-            rescue => e
187
-              outer_e = e
188 219
             end
220
+
221
+            return sock
222
+          rescue => e
223
+            outer_e = e
189 224
           end
190 225
         end
191 226
 
@@ -198,11 +233,21 @@ class Request
198 233
 
199 234
       alias new open
200 235
 
201
-      def thru_hidden_service?(host)
202
-        Rails.configuration.x.access_to_hidden_service && /\.(onion|i2p)$/.match(host)
236
+      def check_private_address(address)
237
+        raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(IPAddr.new(address.to_s))
238
+      end
239
+    end
240
+  end
241
+
242
+  class ProxySocket < Socket
243
+    class << self
244
+      def check_private_address(_address)
245
+        # Accept connections to private addresses as HTTP proxies will usually
246
+        # be on local addresses
247
+        nil
203 248
       end
204 249
     end
205 250
   end
206 251
 
207
-  private_constant :ClientLimit, :Socket
252
+  private_constant :ClientLimit, :Socket, :ProxySocket
208 253
 end

+ 114
- 0
app/lib/request_pool.rb View File

@@ -0,0 +1,114 @@
1
+# frozen_string_literal: true
2
+
3
+require_relative './connection_pool/shared_connection_pool'
4
+
5
+class RequestPool
6
+  def self.current
7
+    @current ||= RequestPool.new
8
+  end
9
+
10
+  class Reaper
11
+    attr_reader :pool, :frequency
12
+
13
+    def initialize(pool, frequency)
14
+      @pool      = pool
15
+      @frequency = frequency
16
+    end
17
+
18
+    def run
19
+      return unless frequency&.positive?
20
+
21
+      Thread.new(frequency, pool) do |t, p|
22
+        loop do
23
+          sleep t
24
+          p.flush
25
+        end
26
+      end
27
+    end
28
+  end
29
+
30
+  MAX_IDLE_TIME = 30
31
+  WAIT_TIMEOUT  = 5
32
+  MAX_POOL_SIZE = ENV.fetch('MAX_REQUEST_POOL_SIZE', 512).to_i
33
+
34
+  class Connection
35
+    attr_reader :site, :last_used_at, :created_at, :in_use, :dead, :fresh
36
+
37
+    def initialize(site)
38
+      @site         = site
39
+      @http_client  = http_client
40
+      @last_used_at = nil
41
+      @created_at   = current_time
42
+      @dead         = false
43
+      @fresh        = true
44
+    end
45
+
46
+    def use
47
+      @last_used_at = current_time
48
+      @in_use       = true
49
+
50
+      retries = 0
51
+
52
+      begin
53
+        yield @http_client
54
+      rescue HTTP::ConnectionError
55
+        # It's possible the connection was closed, so let's
56
+        # try re-opening it once
57
+
58
+        close
59
+
60
+        if @fresh || retries.positive?
61
+          raise
62
+        else
63
+          @http_client = http_client
64
+          retries     += 1
65
+          retry
66
+        end
67
+      rescue StandardError
68
+        # If this connection raises errors of any kind, it's
69
+        # better if it gets reaped as soon as possible
70
+
71
+        close
72
+        @dead = true
73
+        raise
74
+      end
75
+    ensure
76
+      @fresh  = false
77
+      @in_use = false
78
+    end
79
+
80
+    def seconds_idle
81
+      current_time - (@last_used_at || @created_at)
82
+    end
83
+
84
+    def close
85
+      @http_client.close
86
+    end
87
+
88
+    private
89
+
90
+    def http_client
91
+      Request.http_client.persistent(@site, timeout: MAX_IDLE_TIME)
92
+    end
93
+
94
+    def current_time
95
+      Process.clock_gettime(Process::CLOCK_MONOTONIC)
96
+    end
97
+  end
98
+
99
+  def initialize
100
+    @pool   = ConnectionPool::SharedConnectionPool.new(size: MAX_POOL_SIZE, timeout: WAIT_TIMEOUT) { |site| Connection.new(site) }
101
+    @reaper = Reaper.new(self, 30)
102
+    @reaper.run
103
+  end
104
+
105
+  def with(site, &block)
106
+    @pool.with(site) do |connection|
107
+      ActiveSupport::Notifications.instrument('with.request_pool', miss: connection.fresh, host: connection.site) do
108
+        connection.use(&block)
109
+      end
110
+    end
111
+  end
112
+
113
+  delegate :size, :flush, to: :@pool
114
+end

+ 5
- 3
app/lib/sidekiq_error_handler.rb View File

@@ -3,9 +3,11 @@
3 3
 class SidekiqErrorHandler
4 4
   def call(*)
5 5
     yield
6
-  rescue Mastodon::HostValidationError => e
7
-    Rails.logger.error "#{e.class}: #{e.message}"
8
-    Rails.logger.error e.backtrace.join("\n")
6
+  rescue Mastodon::HostValidationError
9 7
     # Do not retry
8
+  ensure
9
+    socket = Thread.current[:statsd_socket]
10
+    socket&.close
11
+    Thread.current[:statsd_socket] = nil
10 12
   end
11 13
 end

+ 6
- 3
app/models/admin/account_action.rb View File

@@ -17,10 +17,13 @@ class Admin::AccountAction
17 17
                 :type,
18 18
                 :text,
19 19
                 :report_id,
20
-                :warning_preset_id,
21
-                :send_email_notification
20
+                :warning_preset_id
22 21
 
23
-  attr_reader :warning
22
+  attr_reader :warning, :send_email_notification
23
+
24
+  def send_email_notification=(value)
25
+    @send_email_notification = ActiveModel::Type::Boolean.new.cast(value)
26
+  end
24 27
 
25 28
   def save!
26 29
     ApplicationRecord.transaction do

+ 3
- 1
app/models/concerns/attachmentable.rb View File

@@ -60,7 +60,9 @@ module Attachmentable
60 60
   end
61 61
 
62 62
   def calculated_content_type(attachment)
63
-    Paperclip.run('file', '-b --mime :file', file: attachment.queued_for_write[:original].path).split(/[:;\s]+/).first.chomp
63
+    content_type = Paperclip.run('file', '-b --mime :file', file: attachment.queued_for_write[:original].path).split(/[:;\s]+/).first.chomp
64
+    content_type = 'video/mp4' if content_type == 'video/x-m4v'
65
+    content_type
64 66
   rescue Terrapin::CommandLineError
65 67
     ''
66 68
   end

+ 7
- 0
app/models/custom_filter.rb View File

@@ -35,6 +35,13 @@ class CustomFilter < ApplicationRecord
35 35
   before_validation :clean_up_contexts
36 36
   after_commit :remove_cache
37 37
 
38
+  def expires_in
39
+    return @expires_in if defined?(@expires_in)
40
+    return nil if expires_at.nil?
41
+
42
+    [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].find { |expires_in| expires_in.from_now >= expires_at }
43
+  end
44
+
38 45
   private
39 46
 
40 47
   def clean_up_contexts

+ 3
- 1
app/services/activitypub/process_account_service.rb View File

@@ -15,6 +15,8 @@ class ActivityPub::ProcessAccountService < BaseService
15 15
     @domain      = domain
16 16
     @collections = {}
17 17
 
18
+    return if auto_suspend?
19
+
18 20
     RedisLock.acquire(lock_options) do |lock|
19 21
       if lock.acquired?
20 22
         @account        = Account.find_remote(@username, @domain)
@@ -55,7 +57,7 @@ class ActivityPub::ProcessAccountService < BaseService
55 57
     @account.domain       = @domain
56 58
     @account.private_key  = nil
57 59
     @account.suspended_at = domain_block.created_at if auto_suspend?
58
-    @account.silenced_at = domain_block.created_at if auto_silence?
60
+    @account.silenced_at  = domain_block.created_at if auto_silence?
59 61
   end
60 62
 
61 63
   def update_account

+ 1
- 1
app/services/resolve_account_service.rb View File

@@ -48,7 +48,7 @@ class ResolveAccountService < BaseService
48 48
       return
49 49
     end
50 50
 
51
-    return if links_missing?
51
+    return if links_missing? || auto_suspend?
52 52
     return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
53 53
 
54 54
     RedisLock.acquire(lock_options) do |lock|

+ 12
- 5
app/workers/activitypub/delivery_worker.rb View File

@@ -17,6 +17,7 @@ class ActivityPub::DeliveryWorker
17 17
     @json           = json
18 18
     @source_account = Account.find(source_account_id)
19 19
     @inbox_url      = inbox_url
20
+    @host           = Addressable::URI.parse(inbox_url).normalized_site
20 21
 
21 22
     perform_request
22 23
 
@@ -28,16 +29,18 @@ class ActivityPub::DeliveryWorker
28 29
 
29 30
   private
30 31
 
31
-  def build_request
32
-    request = Request.new(:post, @inbox_url, body: @json)
32
+  def build_request(http_client)
33
+    request = Request.new(:post, @inbox_url, body: @json, http_client: http_client)
33 34
     request.on_behalf_of(@source_account, :uri, sign_with: @options[:sign_with])
34 35
     request.add_headers(HEADERS)
35 36
   end
36 37
 
37 38
   def perform_request
38 39
     light = Stoplight(@inbox_url) do
39
-      build_request.perform do |response|
40
-        raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response)
40
+      request_pool.with(@host) do |http_client|
41
+        build_request(http_client).perform do |response|
42
+          raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response)
43
+        end
41 44
       end
42 45
     end
43 46
 
@@ -51,10 +54,14 @@ class ActivityPub::DeliveryWorker
51 54
   end
52 55
 
53 56
   def response_error_unsalvageable?(response)
54
-    (400...500).cover?(response.code) && ![401, 408, 429].include?(response.code)
57
+    response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code))
55 58
   end
56 59
 
57 60
   def failure_tracker
58 61
     @failure_tracker ||= DeliveryFailureTracker.new(@inbox_url)
59 62
   end
63
+
64
+  def request_pool
65
+    RequestPool.current
66
+  end
60 67
 end

+ 0
- 18
config/initializers/instrumentation.rb View File

@@ -1,18 +0,0 @@
1
-# frozen_string_literal: true
2
-
3
-instrumentation_hostname = ENV.fetch('INSTRUMENTATION_HOSTNAME') { 'localhost' }
4
-
5
-ActiveSupport::Notifications.subscribe(/process_action.action_controller/) do |*args|
6
-  event      = ActiveSupport::Notifications::Event.new(*args)
7
-  controller = event.payload[:controller]
8
-  action     = event.payload[:action]
9
-  format     = event.payload[:format] || 'all'
10
-  format     = 'all' if format == '*/*'
11
-  status     = event.payload[:status]
12
-  key        = "#{controller}.#{action}.#{format}.#{instrumentation_hostname}"
13
-
14
-  ActiveSupport::Notifications.instrument :performance, action: :measure, measurement: "#{key}.total_duration", value: event.duration
15
-  ActiveSupport::Notifications.instrument :performance, action: :measure, measurement: "#{key}.db_time", value: event.payload[:db_runtime]
16
-  ActiveSupport::Notifications.instrument :performance, action: :measure, measurement: "#{key}.view_time", value: event.payload[:view_runtime]
17
-  ActiveSupport::Notifications.instrument :performance, measurement: "#{key}.status.#{status}"
18
-end

+ 3
- 3
config/initializers/statsd.rb View File

@@ -3,10 +3,10 @@
3 3
 if ENV['STATSD_ADDR'].present?
4 4
   host, port = ENV['STATSD_ADDR'].split(':')
5 5
 
6
-  statsd = ::Statsd.new(host, port)
7
-  statsd.namespace = ENV.fetch('STATSD_NAMESPACE') { ['Mastodon', Rails.env].join('.') }
6
+  $statsd = ::Statsd.new(host, port)
7
+  $statsd.namespace = ENV.fetch('STATSD_NAMESPACE') { ['Mastodon', Rails.env].join('.') }
8 8
 
9
-  ::NSA.inform_statsd(statsd) do |informant|
9
+  ::NSA.inform_statsd($statsd) do |informant|
10 10
     informant.collect(:action_controller, :web)
11 11
     informant.collect(:active_record, :db)
12 12
     informant.collect(:active_support_cache, :cache)

+ 0
- 2
config/routes.rb View File

@@ -299,7 +299,6 @@ Rails.application.routes.draw do
299 299
 
300 300
         member do
301 301
           get :context
302
-          get :card
303 302
         end
304 303
       end
305 304
 
@@ -362,7 +361,6 @@ Rails.application.routes.draw do
362 361
       resources :notifications, only: [:index, :show, :destroy] do
363 362
         collection do
364 363
           post :clear
365
-          post :dismiss # Deprecated
366 364
           delete :destroy_multiple
367 365
         end
368 366
 

+ 2
- 2
package.json View File

@@ -164,7 +164,7 @@
164 164
     "webpack": "^4.34.0",
165 165
     "webpack-assets-manifest": "^3.1.1",
166 166
     "webpack-bundle-analyzer": "^3.3.2",
167
-    "webpack-cli": "^3.3.4",
167
+    "webpack-cli": "^3.3.5",
168 168
     "webpack-merge": "^4.2.1",
169 169
     "websocket.js": "^0.1.12"
170 170
   },
@@ -176,7 +176,7 @@
176 176
     "eslint": "^5.16.0",
177 177
     "eslint-plugin-import": "~2.17.3",
178 178
     "eslint-plugin-jsx-a11y": "~6.2.1",
179
-    "eslint-plugin-promise": "~4.1.1",
179
+    "eslint-plugin-promise": "~4.2.1",
180 180
     "eslint-plugin-react": "~7.12.1",
181 181
     "jest": "^24.8.0",
182 182
     "raf": "^3.4.1",

+ 0
- 14
spec/controllers/api/v1/statuses_controller_spec.rb View File

@@ -91,13 +91,6 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
91 91
           expect(response).to have_http_status(404)
92 92
         end
93 93
       end
94
-
95
-      describe 'GET #card' do
96
-        it 'returns http unautharized' do
97
-          get :card, params: { id: status.id }
98
-          expect(response).to have_http_status(404)
99
-        end
100
-      end
101 94
     end
102 95
 
103 96
     context 'with a public status' do
@@ -120,13 +113,6 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
120 113
           expect(response).to have_http_status(200)
121 114
         end
122 115
       end
123
-
124
-      describe 'GET #card' do
125
-        it 'returns http success' do
126
-          get :card, params: { id: status.id }
127
-          expect(response).to have_http_status(200)
128
-        end
129
-      end
130 116
     end
131 117
   end
132 118
 end

+ 28
- 0
spec/lib/connection_pool/shared_connection_pool_spec.rb View File

@@ -0,0 +1,28 @@
1
+# frozen_string_literal: true
2
+
3
+require 'rails_helper'
4
+
5
+describe ConnectionPool::SharedConnectionPool do
6
+  class MiniConnection
7
+    attr_reader :site
8
+
9
+    def initialize(site)
10
+      @site = site
11
+    end
12
+  end
13
+
14
+  subject { described_class.new(size: 5, timeout: 5) { |site| MiniConnection.new(site) } }
15
+
16
+  describe '#with' do
17
+    it 'runs a block with a connection' do
18
+      block_run = false
19
+
20
+      subject.with('foo') do |connection|
21
+        expect(connection).to be_a MiniConnection
22
+        block_run = true
23
+      end
24
+
25
+      expect(block_run).to be true
26
+    end
27
+  end
28
+end

+ 61
- 0
spec/lib/connection_pool/shared_timed_stack_spec.rb View File

@@ -0,0 +1,61 @@
1
+# frozen_string_literal: true
2
+
3
+require 'rails_helper'
4
+
5
+describe ConnectionPool::SharedTimedStack do
6
+  class MiniConnection
7
+    attr_reader :site
8
+
9
+    def initialize(site)
10
+      @site = site
11
+    end
12
+  end
13
+
14
+  subject { described_class.new(5) { |site| MiniConnection.new(site) } }
15
+
16
+  describe '#push' do
17
+    it 'keeps the connection in the stack' do
18
+      subject.push(MiniConnection.new('foo'))
19
+      expect(subject.size).to eq 1
20
+    end
21
+  end
22
+
23
+  describe '#pop' do
24
+    it 'returns a connection' do
25
+      expect(subject.pop('foo')).to be_a MiniConnection
26
+    end
27
+
28
+    it 'returns the same connection that was pushed in' do
29
+      connection = MiniConnection.new('foo')
30
+      subject.push(connection)
31
+      expect(subject.pop('foo')).to be connection
32
+    end
33
+
34
+    it 'does not create more than maximum amount of connections' do
35
+      expect { 6.times { subject.pop('foo', 0) } }.to raise_error Timeout::Error
36
+    end
37
+
38
+    it 'repurposes a connection for a different site when maximum amount is reached' do
39
+      5.times { subject.push(MiniConnection.new('foo')) }
40
+      expect(subject.pop('bar')).to be_a MiniConnection
41
+    end
42
+  end
43
+
44
+  describe '#empty?' do
45
+    it 'returns true when no connections on the stack' do
46
+      expect(subject.empty?).to be true
47
+    end
48
+
49
+    it 'returns false when there are connections on the stack' do
50
+      subject.push(MiniConnection.new('foo'))
51
+      expect(subject.empty?).to be false
52
+    end
53
+  end
54
+
55
+  describe '#size' do
56
+    it 'returns the number of connections on the stack' do
57
+      2.times { subject.push(MiniConnection.new('foo')) }
58
+      expect(subject.size).to eq 2
59
+    end
60
+  end
61
+end

+ 63
- 0
spec/lib/request_pool_spec.rb View File

@@ -0,0 +1,63 @@
1
+# frozen_string_literal: true
2
+
3
+require 'rails_helper'
4
+
5
+describe RequestPool do
6
+  subject { described_class.new }
7
+
8
+  describe '#with' do
9
+    it 'returns a HTTP client for a host' do
10
+      subject.with('http://example.com') do |http_client|
11
+        expect(http_client).to be_a HTTP::Client
12
+      end
13
+    end
14
+
15
+    it 'returns the same instance of HTTP client within the same thread for the same host' do
16
+      test_client = nil
17
+
18
+      subject.with('http://example.com') { |http_client| test_client = http_client }
19
+      expect(test_client).to_not be_nil
20
+      subject.with('http://example.com') { |http_client| expect(http_client).to be test_client }
21
+    end
22
+
23
+    it 'returns different HTTP clients for different hosts' do
24
+      test_client = nil
25
+
26
+      subject.with('http://example.com') { |http_client| test_client = http_client }
27
+      expect(test_client).to_not be_nil
28
+      subject.with('http://example.org') { |http_client| expect(http_client).to_not be test_client }
29
+    end
30
+
31
+    it 'grows to the number of threads accessing it' do
32
+      stub_request(:get, 'http://example.com/').to_return(status: 200, body: 'Hello!')
33
+
34
+      subject
35
+
36
+      threads = 20.times.map do |i|
37
+        Thread.new do
38
+          20.times do
39
+            subject.with('http://example.com') do |http_client|
40
+              http_client.get('/').flush
41
+            end
42
+          end
43
+        end
44
+      end
45
+
46
+      threads.map(&:join)
47
+
48
+      expect(subject.size).to be > 1
49
+    end
50
+
51
+    it 'closes idle connections' do
52
+      stub_request(:get, 'http://example.com/').to_return(status: 200, body: 'Hello!')
53
+
54
+      subject.with('http://example.com') do |http_client|
55
+        http_client.get('/').flush
56
+      end
57
+
58
+      expect(subject.size).to eq 1
59
+      sleep RequestPool::MAX_IDLE_TIME + 30 + 1
60
+      expect(subject.size).to eq 0
61
+    end
62
+  end
63
+end

+ 125
- 62
yarn.lock View File

@@ -2274,6 +2274,15 @@ caseless@~0.12.0:
2274 2274
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
2275 2275
   integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
2276 2276
 
2277
+chalk@2.4.2, chalk@^2.0, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2:
2278
+  version "2.4.2"
2279
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
2280
+  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
2281
+  dependencies:
2282
+    ansi-styles "^3.2.1"
2283
+    escape-string-regexp "^1.0.5"
2284
+    supports-color "^5.3.0"
2285
+
2277 2286
 chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
2278 2287
   version "1.1.3"
2279 2288
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@@ -2285,15 +2294,6 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
2285 2294
     strip-ansi "^3.0.0"
2286 2295
     supports-color "^2.0.0"
2287 2296
 
2288
-chalk@^2.0, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2:
2289
-  version "2.4.2"
2290
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
2291
-  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
2292
-  dependencies:
2293
-    ansi-styles "^3.2.1"
2294
-    escape-string-regexp "^1.0.5"
2295
-    supports-color "^5.3.0"
2296
-
2297 2297
 chardet@^0.7.0:
2298 2298
   version "0.7.0"
2299 2299
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
@@ -2408,6 +2408,15 @@ cliui@^4.0.0:
2408 2408
     strip-ansi "^4.0.0"
2409 2409
     wrap-ansi "^2.0.0"
2410 2410
 
2411
+cliui@^5.0.0:
2412
+  version "5.0.0"
2413
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
2414
+  integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
2415
+  dependencies:
2416
+    string-width "^3.1.0"
2417
+    strip-ansi "^5.2.0"
2418
+    wrap-ansi "^5.1.0"
2419
+
2411 2420
 clone-deep@^2.0.1:
2412 2421
   version "2.0.2"
2413 2422
   resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713"
@@ -2741,7 +2750,7 @@ cross-env@^5.1.4:
2741 2750
     cross-spawn "^6.0.5"
2742 2751
     is-windows "^1.0.0"
2743 2752
 
2744
-cross-spawn@^6.0.0, cross-spawn@^6.0.5:
2753
+cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5:
2745 2754
   version "6.0.5"
2746 2755
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
2747 2756
   integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
@@ -3428,7 +3437,7 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0:
3428 3437
   dependencies:
3429 3438
     once "^1.4.0"
3430 3439
 
3431
-enhanced-resolve@^4.1.0:
3440
+enhanced-resolve@4.1.0, enhanced-resolve@^4.1.0:
3432 3441
   version "4.1.0"
3433 3442
   resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f"
3434 3443
   integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==
@@ -3688,10 +3697,10 @@ eslint-plugin-jsx-a11y@~6.2.1:
3688 3697
     has "^1.0.3"
3689 3698
     jsx-ast-utils "^2.0.1"
3690 3699
 
3691
-eslint-plugin-promise@~4.1.1:
3692
-  version "4.1.1"
3693
-  resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz#1e08cb68b5b2cd8839f8d5864c796f56d82746db"
3694
-  integrity sha512-faAHw7uzlNPy7b45J1guyjazw28M+7gJokKUjC5JSFoYfUEyy6Gw/i7YQvmv2Yk00sUjWcmzXQLpU1Ki/C2IZQ==
3700
+eslint-plugin-promise@~4.2.1:
3701
+  version "4.2.1"
3702
+  resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"
3703
+  integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==
3695 3704
 
3696 3705
 eslint-plugin-react@~7.12.1:
3697 3706
   version "7.12.1"
@@ -4208,13 +4217,13 @@ find-up@^3.0.0:
4208 4217
   dependencies:
4209 4218
     locate-path "^3.0.0"
4210 4219
 
4211
-findup-sync@^2.0.0:
4212
-  version "2.0.0"
4213
-  resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc"
4214
-  integrity sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=
4220
+findup-sync@3.0.0:
4221
+  version "3.0.0"
4222
+  resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1"
4223
+  integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==
4215 4224
   dependencies:
4216 4225
     detect-file "^1.0.0"
4217
-    is-glob "^3.1.0"
4226
+    is-glob "^4.0.0"
4218 4227
     micromatch "^3.0.4"
4219 4228
     resolve-dir "^1.0.1"
4220 4229
 
@@ -4437,6 +4446,11 @@ get-caller-file@^1.0.1:
4437 4446
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
4438 4447
   integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
4439 4448
 
4449
+get-caller-file@^2.0.1:
4450
+  version "2.0.5"
4451
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
4452
+  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
4453
+
4440 4454
 get-stream@^4.0.0:
4441 4455
   version "4.1.0"
4442 4456
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
@@ -4476,6 +4490,13 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@~7.1.1:
4476 4490
     once "^1.3.0"
4477 4491
     path-is-absolute "^1.0.0"
4478 4492
 
4493
+global-modules@2.0.0:
4494
+  version "2.0.0"
4495
+  resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
4496
+  integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==
4497
+  dependencies:
4498
+    global-prefix "^3.0.0"
4499
+
4479 4500
 global-modules@^1.0.0:
4480 4501
   version "1.0.0"
4481 4502
   resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
@@ -4496,6 +4517,15 @@ global-prefix@^1.0.1:
4496 4517
     is-windows "^1.0.1"
4497 4518
     which "^1.2.14"
4498 4519
 
4520
+global-prefix@^3.0.0:
4521
+  version "3.0.0"
4522
+  resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97"
4523
+  integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==
4524
+  dependencies:
4525
+    ini "^1.3.5"
4526
+    kind-of "^6.0.2"
4527
+    which "^1.3.1"
4528
+
4499 4529
 globals@^11.1.0, globals@^11.7.0:
4500 4530
   version "11.12.0"
4501 4531
   resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@@ -4921,7 +4951,7 @@ import-from@^2.1.0:
4921 4951
   dependencies:
4922 4952
     resolve-from "^3.0.0"
4923 4953
 
4924
-import-local@^2.0.0:
4954
+import-local@2.0.0, import-local@^2.0.0:
4925 4955
   version "2.0.0"
4926 4956
   resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
4927 4957
   integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==
@@ -4970,7 +5000,7 @@ inherits@2.0.1:
4970 5000
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
4971 5001
   integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
4972 5002
 
4973
-ini@^1.3.4, ini@~1.3.0:
5003
+ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
4974 5004
   version "1.3.5"
4975 5005
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
4976 5006
   integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
@@ -5021,7 +5051,7 @@ internal-ip@^4.3.0:
5021 5051
     default-gateway "^4.2.0"
5022 5052
     ipaddr.js "^1.9.0"
5023 5053
 
5024
-interpret@^1.1.0:
5054
+interpret@1.2.0:
5025 5055
   version "1.2.0"
5026 5056
   resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
5027 5057
   integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
@@ -6119,7 +6149,7 @@ loader-utils@0.2.x:
6119 6149
     json5 "^0.5.0"
6120 6150
     object-assign "^4.0.1"
6121 6151
 
6122
-loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.2, loader-utils@^1.2.3:
6152
+loader-utils@1.2.3, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.2, loader-utils@^1.2.3:
6123 6153
   version "1.2.3"
6124 6154
   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
6125 6155
   integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==
@@ -7034,7 +7064,7 @@ os-homedir@^1.0.0:
7034 7064
   resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
7035 7065
   integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
7036 7066
 
7037
-os-locale@^3.0.0:
7067
+os-locale@^3.0.0, os-locale@^3.1.0:
7038 7068
   version "3.1.0"
7039 7069
   resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
7040 7070
   integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
@@ -7831,11 +7861,6 @@ prepend-http@^1.0.0:
7831 7861
   resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
7832 7862
   integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
7833 7863
 
7834
-prettier@^1.17.0:
7835
-  version "1.18.2"
7836
-  resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea"
7837
-  integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==
7838
-
7839 7864
 pretty-format@^24.8.0:
7840 7865
   version "24.8.0"
7841 7866
   resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2"
@@ -8650,6 +8675,11 @@ require-main-filename@^1.0.1:
8650 8675
   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
8651 8676
   integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
8652 8677
 
8678
+require-main-filename@^2.0.0:
8679
+  version "2.0.0"
8680
+  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
8681
+  integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
8682
+
8653 8683
 require-package-name@^2.0.1:
8654 8684
   version "2.0.1"
8655 8685
   resolved "https://registry.yarnpkg.com/require-package-name/-/require-package-name-2.0.1.tgz#c11e97276b65b8e2923f75dabf5fb2ef0c3841b9"
@@ -9411,7 +9441,7 @@ string-width@^1.0.1:
9411 9441
     is-fullwidth-code-point "^2.0.0"
9412 9442
     strip-ansi "^4.0.0"
9413 9443
 
9414
-string-width@^3.0.0:
9444
+string-width@^3.0.0, string-width@^3.1.0:
9415 9445
   version "3.1.0"
9416 9446
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
9417 9447
   integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
@@ -9464,7 +9494,7 @@ strip-ansi@^4.0.0:
9464 9494
   dependencies:
9465 9495
     ansi-regex "^3.0.0"
9466 9496
 
9467
-strip-ansi@^5.0.0, strip-ansi@^5.1.0:
9497
+strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
9468 9498
   version "5.2.0"
9469 9499
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
9470 9500
   integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
@@ -9515,6 +9545,13 @@ substring-trie@^1.0.2:
9515 9545
   resolved "https://registry.yarnpkg.com/substring-trie/-/substring-trie-1.0.2.tgz#7b42592391628b4f2cb17365c6cce4257c7b7af5"
9516 9546
   integrity sha1-e0JZI5Fii08ssXNlxszkJXx7evU=
9517 9547
 
9548
+supports-color@6.1.0, supports-color@^6.0.0, supports-color@^6.1.0:
9549
+  version "6.1.0"
9550
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
9551
+  integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
9552
+  dependencies:
9553
+    has-flag "^3.0.0"
9554
+
9518 9555
 supports-color@^2.0.0:
9519 9556
   version "2.0.0"
9520 9557
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -9527,20 +9564,13 @@ supports-color@^3.2.3:
9527 9564
   dependencies:
9528 9565
     has-flag "^1.0.0"
9529 9566
 
9530
-supports-color@^5.3.0, supports-color@^5.5.0:
9567
+supports-color@^5.3.0:
9531 9568
   version "5.5.0"
9532 9569
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
9533 9570
   integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
9534 9571
   dependencies:
9535 9572
     has-flag "^3.0.0"
9536 9573
 
9537
-supports-color@^6.0.0, supports-color@^6.1.0:
9538
-  version "6.1.0"
9539
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
9540
-  integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
9541
-  dependencies:
9542
-    has-flag "^3.0.0"
9543
-
9544 9574
 svgo@^1.0.0:
9545 9575
   version "1.1.1"
9546 9576
   resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.1.1.tgz#12384b03335bcecd85cfa5f4e3375fed671cb985"
@@ -10017,10 +10047,10 @@ uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2:
10017 10047
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
10018 10048
   integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
10019 10049
 
10020
-v8-compile-cache@^2.0.2:
10021
-  version "2.0.2"
10022
-  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c"
10023
-  integrity sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw==
10050
+v8-compile-cache@2.0.3:
10051
+  version "2.0.3"
10052
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"
10053
+  integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==
10024 10054
 
10025 10055
 validate-npm-package-license@^3.0.1:
10026 10056
   version "3.0.4"
@@ -10142,23 +10172,22 @@ webpack-bundle-analyzer@^3.3.2:
10142 10172
     opener "^1.5.1"
10143 10173
     ws "^6.0.0"
10144 10174
 
10145
-webpack-cli@^3.3.4:
10146
-  version "3.3.4"
10147
-  resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.4.tgz#de27e281c48a897b8c219cb093e261d5f6afe44a"
10148
-  integrity sha512-ubJGQEKMtBSpT+LiL5hXvn2GIOWiRWItR1DGUqJRhwRBeGhpRXjvF5f0erqdRJLErkfqS5/Ldkkedh4AL5Q1ZQ==
10149
-  dependencies:
10150
-    chalk "^2.4.1"
10151
-    cross-spawn "^6.0.5"
10152
-    enhanced-resolve "^4.1.0"
10153
-    findup-sync "^2.0.0"
10154
-    global-modules "^1.0.0"
10155
-    import-local "^2.0.0"
10156
-    interpret "^1.1.0"
10157
-    loader-utils "^1.1.0"
10158
-    prettier "^1.17.0"
10159
-    supports-color "^5.5.0"
10160
-    v8-compile-cache "^2.0.2"
10161
-    yargs "^12.0.5"
10175
+webpack-cli@^3.3.5:
10176
+  version "3.3.5"
10177
+  resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.5.tgz#f4d1238a66a2843d9cebf189835ea22142e72767"
10178
+  integrity sha512-w0j/s42c5UhchwTmV/45MLQnTVwRoaUTu9fM5LuyOd/8lFoCNCELDogFoecx5NzRUndO0yD/gF2b02XKMnmAWQ==
10179
+  dependencies:
10180
+    chalk "2.4.2"
10181
+    cross-spawn "6.0.5"
10182
+    enhanced-resolve "4.1.0"
10183
+    findup-sync "3.0.0"
10184
+    global-modules "2.0.0"
10185
+    import-local "2.0.0"
10186
+    interpret "1.2.0"
10187
+    loader-utils "1.2.3"
10188
+    supports-color "6.1.0"
10189
+    v8-compile-cache "2.0.3"
10190
+    yargs "13.2.4"
10162 10191
 
10163 10192
 webpack-dev-middleware@^3.7.0:
10164 10193
   version "3.7.0"
@@ -10320,7 +10349,7 @@ which-module@^2.0.0:
10320 10349
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
10321 10350
   integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
10322 10351
 
10323
-which@^1.2.14, which@^1.2.9, which@^1.3.0:
10352
+which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1:
10324 10353
   version "1.3.1"
10325 10354
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
10326 10355
   integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
@@ -10359,6 +10388,15 @@ wrap-ansi@^2.0.0:
10359 10388
     string-width "^1.0.1"
10360 10389
     strip-ansi "^3.0.1"
10361 10390
 
10391
+wrap-ansi@^5.1.0:
10392
+  version "5.1.0"
10393
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
10394
+  integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
10395
+  dependencies:
10396
+    ansi-styles "^3.2.0"
10397
+    string-width "^3.0.0"
10398
+    strip-ansi "^5.0.0"
10399
+
10362 10400
 wrappy@1:
10363 10401
   version "1.0.2"
10364 10402
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@@ -10429,6 +10467,14 @@ yargs-parser@^11.1.1:
10429 10467
     camelcase "^5.0.0"
10430 10468
     decamelize "^1.2.0"
10431 10469
 
10470
+yargs-parser@^13.1.0:
10471
+  version "13.1.1"
10472
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0"
10473
+  integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==
10474
+  dependencies:
10475
+    camelcase "^5.0.0"
10476
+    decamelize "^1.2.0"
10477
+
10432 10478
 yargs@12.0.5, yargs@^12.0.2, yargs@^12.0.5:
10433 10479
   version "12.0.5"
10434 10480
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
@@ -10446,3 +10492,20 @@ yargs@12.0.5, yargs@^12.0.2, yargs@^12.0.5:
10446 10492
     which-module "^2.0.0"
10447 10493
     y18n "^3.2.1 || ^4.0.0"
10448 10494
     yargs-parser "^11.1.1"
10495
+
10496
+yargs@13.2.4:
10497
+  version "13.2.4"
10498
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83"
10499
+  integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==
10500
+  dependencies:
10501
+    cliui "^5.0.0"
10502
+    find-up "^3.0.0"
10503
+    get-caller-file "^2.0.1"
10504
+    os-locale "^3.1.0"
10505
+    require-directory "^2.1.1"
10506
+    require-main-filename "^2.0.0"
10507
+    set-blocking "^2.0.0"
10508
+    string-width "^3.0.0"
10509
+    which-module "^2.0.0"
10510
+    y18n "^4.0.0"
10511
+    yargs-parser "^13.1.0"

Loading…
Cancel
Save