puppeteer-ruby 0.43.0 → 0.43.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -1
- data/docs/api_coverage.md +1 -1
- data/lib/puppeteer/browser.rb +1 -1
- data/lib/puppeteer/chrome_target_manager.rb +28 -10
- data/lib/puppeteer/connection.rb +6 -3
- data/lib/puppeteer/dom_world.rb +0 -43
- data/lib/puppeteer/element_handle.rb +9 -30
- data/lib/puppeteer/frame.rb +16 -9
- data/lib/puppeteer/frame_manager.rb +4 -4
- data/lib/puppeteer/lifecycle_watcher.rb +22 -3
- data/lib/puppeteer/query_handler_manager.rb +35 -0
- data/lib/puppeteer/target.rb +2 -2
- data/lib/puppeteer/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e723d5efd8074f8c70f1b94de1399f35ec39eca88fd86d1991b036db32d783a8
|
4
|
+
data.tar.gz: 463781ad3737b5db0f09b2ba9ec2ed8f62863991c011291437230c512671e0ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 46462c2894970d02792d5021088252fce262f41f81531449a73d9d40745ede1742fe187cde65cae64222ba817366500d347ca310aa658147040e4fbfa5d1b5db
|
7
|
+
data.tar.gz: d5b6e7ae1e0efc6c1e69e7bb136f90dfdc1c2370c29ec4bf9a6877c22bbe38cd6a81f698f086dce3157800bdfc7bfcb06f7eb35b518434870fd95456dc1fbe3c
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,11 @@
|
|
1
|
-
### main [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.43.
|
1
|
+
### main [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.43.1...main)]
|
2
2
|
|
3
3
|
- xxx
|
4
4
|
|
5
|
+
### 0.43.1 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.43.0...0.43.1)]
|
6
|
+
|
7
|
+
- Port Puppeteer v16.1 features, including bugfix and XPath query handler.
|
8
|
+
|
5
9
|
### 0.43.0 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.42.0...0.43.0)]
|
6
10
|
|
7
11
|
- Port Puppeteer v16.0 features. Increasing stability.
|
data/docs/api_coverage.md
CHANGED
data/lib/puppeteer/browser.rb
CHANGED
@@ -196,7 +196,7 @@ class Puppeteer::Browser
|
|
196
196
|
session: session,
|
197
197
|
browser_context: context,
|
198
198
|
target_manager: @target_manager,
|
199
|
-
session_factory: -> { @connection.create_session(target_info) },
|
199
|
+
session_factory: -> (auto_attach_emulated) { @connection.create_session(target_info, auto_attach_emulated: auto_attach_emulated) },
|
200
200
|
ignore_https_errors: @ignore_https_errors,
|
201
201
|
default_viewport: @default_viewport,
|
202
202
|
is_page_target_callback: @is_page_target_callback,
|
@@ -1,4 +1,5 @@
|
|
1
1
|
class Puppeteer::ChromeTargetManager
|
2
|
+
include Puppeteer::DebugPrint
|
2
3
|
include Puppeteer::EventCallbackable
|
3
4
|
|
4
5
|
def initialize(connection:, target_factory:, target_filter_callback:)
|
@@ -33,20 +34,34 @@ class Puppeteer::ChromeTargetManager
|
|
33
34
|
)
|
34
35
|
|
35
36
|
setup_attachment_listeners(@connection)
|
36
|
-
@connection.async_send_message('Target.setDiscoverTargets',
|
37
|
+
@connection.async_send_message('Target.setDiscoverTargets', {
|
38
|
+
discover: true,
|
39
|
+
filter: [
|
40
|
+
{ type: 'tab', exclude: true },
|
41
|
+
{},
|
42
|
+
],
|
43
|
+
}).then do
|
44
|
+
store_existing_targets_for_init
|
45
|
+
end.rescue do |err|
|
46
|
+
debug_puts(err)
|
47
|
+
end
|
37
48
|
end
|
38
49
|
|
39
|
-
def
|
50
|
+
private def store_existing_targets_for_init
|
40
51
|
@discovered_targets_by_target_id.each do |target_id, target_info|
|
41
|
-
if @target_filter_callback.call(target_info)
|
52
|
+
if @target_filter_callback.call(target_info) && target_info.type != 'browser'
|
42
53
|
@target_ids_for_init << target_id
|
43
54
|
end
|
44
55
|
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def init
|
45
59
|
@connection.send_message('Target.setAutoAttach', {
|
46
60
|
waitForDebuggerOnStart: true,
|
47
61
|
flatten: true,
|
48
62
|
autoAttach: true,
|
49
63
|
})
|
64
|
+
finish_initialization_if_ready
|
50
65
|
@initialize_promise.value!
|
51
66
|
end
|
52
67
|
|
@@ -114,7 +129,7 @@ class Puppeteer::ChromeTargetManager
|
|
114
129
|
# Special case (https://crbug.com/1338156): currently, shared_workers
|
115
130
|
# don't get auto-attached. This should be removed once the auto-attach
|
116
131
|
# works.
|
117
|
-
@connection.create_session(target_info)
|
132
|
+
@connection.create_session(target_info, auto_attach_emulated: true)
|
118
133
|
end
|
119
134
|
end
|
120
135
|
|
@@ -170,6 +185,8 @@ class Puppeteer::ChromeTargetManager
|
|
170
185
|
end
|
171
186
|
}
|
172
187
|
|
188
|
+
return unless @connection.auto_attached?(target_info.target_id)
|
189
|
+
|
173
190
|
# Special case for service workers: being attached to service workers will
|
174
191
|
# prevent them from ever being destroyed. Therefore, we silently detach
|
175
192
|
# from service workers unless the connection was manually created via
|
@@ -197,6 +214,8 @@ class Puppeteer::ChromeTargetManager
|
|
197
214
|
return
|
198
215
|
end
|
199
216
|
|
217
|
+
is_existing_target = @attached_targets_by_target_id.has_key?(target_info.target_id)
|
218
|
+
|
200
219
|
target = @attached_targets_by_target_id[target_info.target_id] || @target_factory.call(target_info, session)
|
201
220
|
setup_attachment_listeners(session)
|
202
221
|
|
@@ -218,11 +237,10 @@ class Puppeteer::ChromeTargetManager
|
|
218
237
|
end
|
219
238
|
|
220
239
|
@target_ids_for_init.delete(target.target_id)
|
221
|
-
|
222
|
-
|
223
|
-
if @target_ids_for_init.empty?
|
224
|
-
@initialize_promise.fulfill(nil) unless @initialize_promise.resolved?
|
240
|
+
unless is_existing_target
|
241
|
+
future { emit_event(TargetManagerEmittedEvents::TargetAvailable, target) }
|
225
242
|
end
|
243
|
+
finish_initialization_if_ready
|
226
244
|
|
227
245
|
future do
|
228
246
|
# TODO: the browser might be shutting down here. What do we do with the error?
|
@@ -239,8 +257,8 @@ class Puppeteer::ChromeTargetManager
|
|
239
257
|
end
|
240
258
|
end
|
241
259
|
|
242
|
-
private def finish_initialization_if_ready(target_id)
|
243
|
-
@target_ids_for_init.delete(target_id)
|
260
|
+
private def finish_initialization_if_ready(target_id = nil)
|
261
|
+
@target_ids_for_init.delete(target_id) if target_id
|
244
262
|
if @target_ids_for_init.empty?
|
245
263
|
@initialize_promise.fulfill(nil) unless @initialize_promise.resolved?
|
246
264
|
end
|
data/lib/puppeteer/connection.rb
CHANGED
@@ -319,15 +319,18 @@ class Puppeteer::Connection
|
|
319
319
|
end
|
320
320
|
|
321
321
|
def auto_attached?(target_id)
|
322
|
-
|
322
|
+
!@manually_attached.include?(target_id)
|
323
323
|
end
|
324
324
|
|
325
325
|
# @param {Protocol.Target.TargetInfo} targetInfo
|
326
326
|
# @return [CDPSession]
|
327
|
-
def create_session(target_info)
|
328
|
-
|
327
|
+
def create_session(target_info, auto_attach_emulated: false)
|
328
|
+
unless auto_attach_emulated
|
329
|
+
@manually_attached << target_info.target_id
|
330
|
+
end
|
329
331
|
result = send_message('Target.attachToTarget', targetId: target_info.target_id, flatten: true)
|
330
332
|
session_id = result['sessionId']
|
333
|
+
@manually_attached.delete(target_info.target_id)
|
331
334
|
@sessions[session_id]
|
332
335
|
end
|
333
336
|
end
|
data/lib/puppeteer/dom_world.rb
CHANGED
@@ -539,49 +539,6 @@ class Puppeteer::DOMWorld
|
|
539
539
|
handle.as_element
|
540
540
|
end
|
541
541
|
|
542
|
-
# @param xpath [String]
|
543
|
-
# @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
|
544
|
-
# @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
|
545
|
-
# @param timeout [Integer]
|
546
|
-
def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil, root: nil)
|
547
|
-
option_wait_for_visible = visible || false
|
548
|
-
option_wait_for_hidden = hidden || false
|
549
|
-
option_timeout = timeout || @timeout_settings.timeout
|
550
|
-
option_root = root
|
551
|
-
|
552
|
-
polling =
|
553
|
-
if option_wait_for_visible || option_wait_for_hidden
|
554
|
-
'raf'
|
555
|
-
else
|
556
|
-
'mutation'
|
557
|
-
end
|
558
|
-
title = "XPath #{xpath}#{option_wait_for_hidden ? 'to be hidden' : ''}"
|
559
|
-
|
560
|
-
xpath_predicate = make_predicate_string(
|
561
|
-
predicate_arg_def: '(root, selector, waitForVisible, waitForHidden)',
|
562
|
-
predicate_body: <<~JAVASCRIPT
|
563
|
-
const node = document.evaluate(selector, root, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
564
|
-
return checkWaitForOptions(node, waitForVisible, waitForHidden);
|
565
|
-
JAVASCRIPT
|
566
|
-
)
|
567
|
-
|
568
|
-
wait_task = Puppeteer::WaitTask.new(
|
569
|
-
dom_world: self,
|
570
|
-
predicate_body: xpath_predicate,
|
571
|
-
title: title,
|
572
|
-
polling: polling,
|
573
|
-
timeout: option_timeout,
|
574
|
-
args: [xpath, option_wait_for_visible, option_wait_for_hidden],
|
575
|
-
root: option_root,
|
576
|
-
)
|
577
|
-
handle = wait_task.await_promise
|
578
|
-
unless handle.as_element
|
579
|
-
handle.dispose
|
580
|
-
return nil
|
581
|
-
end
|
582
|
-
handle.as_element
|
583
|
-
end
|
584
|
-
|
585
542
|
# @param page_function [String]
|
586
543
|
# @param args [Array]
|
587
544
|
# @param polling [Integer|String]
|
@@ -125,28 +125,14 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
|
|
125
125
|
# Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default
|
126
126
|
# value can be changed by using the {@link Page.setDefaultTimeout} method.
|
127
127
|
def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
|
128
|
-
frame = @context.frame
|
129
|
-
|
130
|
-
secondary_world = frame.secondary_world
|
131
|
-
adopted_root = secondary_world.execution_context.adopt_element_handle(self)
|
132
128
|
param_xpath =
|
133
129
|
if xpath.start_with?('//')
|
134
130
|
".#{xpath}"
|
135
131
|
else
|
136
132
|
xpath
|
137
133
|
end
|
138
|
-
unless param_xpath.start_with?('.//')
|
139
|
-
adopted_root.dispose
|
140
|
-
raise ArgumentError.new("Unsupported xpath expression: #{xpath}")
|
141
|
-
end
|
142
|
-
handle = secondary_world.wait_for_xpath(param_xpath, visible: visible, hidden: hidden, timeout: timeout, root: adopted_root)
|
143
|
-
adopted_root.dispose
|
144
|
-
return nil unless handle
|
145
134
|
|
146
|
-
|
147
|
-
result = main_world.execution_context.adopt_element_handle(handle)
|
148
|
-
handle.dispose
|
149
|
-
result
|
135
|
+
wait_for_selector("xpath/#{param_xpath}", visible: visible, hidden: hidden, timeout: timeout)
|
150
136
|
end
|
151
137
|
|
152
138
|
define_async_method :async_wait_for_xpath
|
@@ -623,21 +609,14 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
|
|
623
609
|
# @param expression [String]
|
624
610
|
# @return [Array<ElementHandle>]
|
625
611
|
def Sx(expression)
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
return array;
|
635
|
-
}
|
636
|
-
JAVASCRIPT
|
637
|
-
handles = evaluate_handle(fn, expression)
|
638
|
-
properties = handles.properties
|
639
|
-
handles.dispose
|
640
|
-
properties.values.map(&:as_element).compact
|
612
|
+
param_xpath =
|
613
|
+
if expression.start_with?('//')
|
614
|
+
".#{expression}"
|
615
|
+
else
|
616
|
+
expression
|
617
|
+
end
|
618
|
+
|
619
|
+
query_selector_all("xpath/#{param_xpath}")
|
641
620
|
end
|
642
621
|
|
643
622
|
define_async_method :async_Sx
|
data/lib/puppeteer/frame.rb
CHANGED
@@ -106,7 +106,14 @@ class Puppeteer::Frame
|
|
106
106
|
# @param {string} expression
|
107
107
|
# @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
|
108
108
|
def Sx(expression)
|
109
|
-
|
109
|
+
param_xpath =
|
110
|
+
if expression.start_with?('//')
|
111
|
+
".#{expression}"
|
112
|
+
else
|
113
|
+
expression
|
114
|
+
end
|
115
|
+
|
116
|
+
query_selector_all("xpath/#{param_xpath}")
|
110
117
|
end
|
111
118
|
|
112
119
|
define_async_method :async_Sx
|
@@ -274,14 +281,14 @@ class Puppeteer::Frame
|
|
274
281
|
# @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
|
275
282
|
# @param timeout [Integer]
|
276
283
|
def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
284
|
+
param_xpath =
|
285
|
+
if xpath.start_with?('//')
|
286
|
+
".#{xpath}"
|
287
|
+
else
|
288
|
+
xpath
|
289
|
+
end
|
290
|
+
|
291
|
+
wait_for_selector("xpath/#{param_xpath}", visible: visible, hidden: hidden, timeout: timeout)
|
285
292
|
end
|
286
293
|
|
287
294
|
define_async_method :async_wait_for_xpath
|
@@ -144,13 +144,13 @@ class Puppeteer::FrameManager
|
|
144
144
|
watcher.same_document_navigation_promise
|
145
145
|
end,
|
146
146
|
)
|
147
|
+
|
148
|
+
watcher.navigation_response
|
147
149
|
rescue Puppeteer::TimeoutError => err
|
148
150
|
raise NavigationError.new(err)
|
149
151
|
ensure
|
150
152
|
watcher.dispose
|
151
153
|
end
|
152
|
-
|
153
|
-
watcher.navigation_response
|
154
154
|
end
|
155
155
|
|
156
156
|
# @param timeout [number|nil]
|
@@ -168,13 +168,13 @@ class Puppeteer::FrameManager
|
|
168
168
|
watcher.same_document_navigation_promise,
|
169
169
|
watcher.new_document_navigation_promise,
|
170
170
|
)
|
171
|
+
|
172
|
+
watcher.navigation_response
|
171
173
|
rescue Puppeteer::TimeoutError => err
|
172
174
|
raise NavigationError.new(err)
|
173
175
|
ensure
|
174
176
|
watcher.dispose
|
175
177
|
end
|
176
|
-
|
177
|
-
watcher.navigation_response
|
178
178
|
end
|
179
179
|
|
180
180
|
# @param event [Hash]
|
@@ -81,7 +81,10 @@ class Puppeteer::LifecycleWatcher
|
|
81
81
|
@frame_manager.add_event_listener(FrameManagerEmittedEvents::FrameSwapped, &method(:handle_frame_swapped)),
|
82
82
|
@frame_manager.add_event_listener(FrameManagerEmittedEvents::FrameDetached, &method(:handle_frame_detached)),
|
83
83
|
]
|
84
|
-
@listener_ids['network_manager'] =
|
84
|
+
@listener_ids['network_manager'] = [
|
85
|
+
@frame_manager.network_manager.add_event_listener(NetworkManagerEmittedEvents::Request, &method(:handle_request)),
|
86
|
+
@frame_manager.network_manager.add_event_listener(NetworkManagerEmittedEvents::Response, &method(:handle_response)),
|
87
|
+
]
|
85
88
|
|
86
89
|
@same_document_navigation_promise = resolvable_future
|
87
90
|
@lifecycle_promise = resolvable_future
|
@@ -90,10 +93,24 @@ class Puppeteer::LifecycleWatcher
|
|
90
93
|
check_lifecycle_complete
|
91
94
|
end
|
92
95
|
|
96
|
+
class AnotherRequestReceivedError < StandardError ; end
|
97
|
+
|
93
98
|
# @param [Puppeteer::HTTPRequest] request
|
94
99
|
def handle_request(request)
|
95
100
|
return if request.frame != @frame || !request.navigation_request?
|
96
101
|
@navigation_request = request
|
102
|
+
@navigation_response_received&.reject(AnotherRequestReceivedError.new('New navigation request was received'))
|
103
|
+
@navigation_response_received = resolvable_future
|
104
|
+
if request.response && !@navigation_response_received.resolved?
|
105
|
+
@navigation_response_received.fulfill(nil)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# @param [Puppeteer::HTTPResponse] response
|
110
|
+
def handle_response(response)
|
111
|
+
return if @navigation_request&.internal&.request_id != response.request.internal.request_id
|
112
|
+
|
113
|
+
@navigation_response_received.fulfill(nil) unless @navigation_response_received.resolved?
|
97
114
|
end
|
98
115
|
|
99
116
|
# @param frame [Puppeteer::Frame]
|
@@ -107,6 +124,8 @@ class Puppeteer::LifecycleWatcher
|
|
107
124
|
|
108
125
|
# @return [Puppeteer::HTTPResponse]
|
109
126
|
def navigation_response
|
127
|
+
# Continue with a possibly null response.
|
128
|
+
@navigation_response_received.value! rescue nil
|
110
129
|
if_present(@navigation_request) do |request|
|
111
130
|
request.response
|
112
131
|
end
|
@@ -175,8 +194,8 @@ class Puppeteer::LifecycleWatcher
|
|
175
194
|
if_present(@listener_ids['frame_manager']) do |ids|
|
176
195
|
@frame_manager.remove_event_listener(*ids)
|
177
196
|
end
|
178
|
-
if_present(@listener_ids['network_manager']) do |
|
179
|
-
@frame_manager.network_manager.remove_event_listener(
|
197
|
+
if_present(@listener_ids['network_manager']) do |ids|
|
198
|
+
@frame_manager.network_manager.remove_event_listener(*ids)
|
180
199
|
end
|
181
200
|
end
|
182
201
|
end
|
@@ -6,6 +6,7 @@ class Puppeteer::QueryHandlerManager
|
|
6
6
|
def query_handlers
|
7
7
|
@query_handlers ||= {
|
8
8
|
aria: Puppeteer::AriaQueryHandler.new,
|
9
|
+
xpath: xpath_handler,
|
9
10
|
}
|
10
11
|
end
|
11
12
|
|
@@ -16,6 +17,40 @@ class Puppeteer::QueryHandlerManager
|
|
16
17
|
)
|
17
18
|
end
|
18
19
|
|
20
|
+
private def xpath_handler
|
21
|
+
@xpath_handler ||= Puppeteer::CustomQueryHandler.new(
|
22
|
+
query_one: <<~JAVASCRIPT,
|
23
|
+
(element, selector) => {
|
24
|
+
const doc = element.ownerDocument || document;
|
25
|
+
const result = doc.evaluate(
|
26
|
+
selector,
|
27
|
+
element,
|
28
|
+
null,
|
29
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE
|
30
|
+
);
|
31
|
+
return result.singleNodeValue;
|
32
|
+
}
|
33
|
+
JAVASCRIPT
|
34
|
+
query_all: <<~JAVASCRIPT,
|
35
|
+
(element, selector) => {
|
36
|
+
const doc = element.ownerDocument || document;
|
37
|
+
const iterator = doc.evaluate(
|
38
|
+
selector,
|
39
|
+
element,
|
40
|
+
null,
|
41
|
+
XPathResult.ORDERED_NODE_ITERATOR_TYPE
|
42
|
+
);
|
43
|
+
const array = [];
|
44
|
+
let item;
|
45
|
+
while ((item = iterator.iterateNext())) {
|
46
|
+
array.push(item);
|
47
|
+
}
|
48
|
+
return array;
|
49
|
+
}
|
50
|
+
JAVASCRIPT
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
19
54
|
class Result
|
20
55
|
def initialize(query_handler:, selector:)
|
21
56
|
@query_handler = query_handler
|
data/lib/puppeteer/target.rb
CHANGED
@@ -93,7 +93,7 @@ class Puppeteer::Target
|
|
93
93
|
end
|
94
94
|
|
95
95
|
def create_cdp_session
|
96
|
-
@session_factory.call
|
96
|
+
@session_factory.call(false)
|
97
97
|
end
|
98
98
|
|
99
99
|
def target_manager
|
@@ -102,7 +102,7 @@ class Puppeteer::Target
|
|
102
102
|
|
103
103
|
def page
|
104
104
|
if @is_page_target_callback.call(@target_info) && @page.nil?
|
105
|
-
client = @session || @session_factory.call
|
105
|
+
client = @session || @session_factory.call(true)
|
106
106
|
@page = Puppeteer::Page.create(client, self, @ignore_https_errors, @default_viewport)
|
107
107
|
end
|
108
108
|
@page
|
data/lib/puppeteer/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: puppeteer-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.43.
|
4
|
+
version: 0.43.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- YusukeIwaki
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-09-
|
11
|
+
date: 2022-09-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|