phlex-reactive 0.12.3 → 0.12.5
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 +95 -0
- data/README.md +54 -7
- data/app/controllers/phlex/reactive/actions_controller.rb +19 -6
- data/app/javascript/phlex/reactive/compute.js +7 -1
- data/app/javascript/phlex/reactive/compute.min.js.map +2 -2
- data/app/javascript/phlex/reactive/reactive_controller.js +254 -13
- data/app/javascript/phlex/reactive/reactive_controller.min.js +2 -2
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +3 -3
- data/lib/phlex/reactive/component/dsl.rb +1 -1
- data/lib/phlex/reactive/component/helpers.rb +9 -0
- data/lib/phlex/reactive/component.rb +8 -1
- data/lib/phlex/reactive/js.rb +29 -0
- data/lib/phlex/reactive/response.rb +5 -1
- data/lib/phlex/reactive/show_conditions.rb +1 -2
- data/lib/phlex/reactive/streamable.rb +44 -28
- data/lib/phlex/reactive/test_helpers/system.rb +1 -2
- data/lib/phlex/reactive/version.rb +1 -1
- data/lib/phlex/reactive.rb +50 -0
- metadata +5 -5
|
@@ -267,7 +267,7 @@ module Phlex
|
|
|
267
267
|
assert_no_scope_double_nesting!(scope, params, name.to_sym) if scope
|
|
268
268
|
Registry.write_entry(
|
|
269
269
|
self, :actions, name.to_sym,
|
|
270
|
-
|
|
270
|
+
ActionDefinition.new(name: name.to_sym, params: params, schema: Phlex::Reactive::ParamSchema.compile(params))
|
|
271
271
|
)
|
|
272
272
|
end
|
|
273
273
|
|
|
@@ -85,6 +85,10 @@ module Phlex
|
|
|
85
85
|
# JS), so the client's attr check would never fire (the on()/warn_unsaved
|
|
86
86
|
# precedent). Off by default → no key, no string, zero client surface.
|
|
87
87
|
data[:reactive_debug] = "true" if Phlex::Reactive.debug
|
|
88
|
+
# Issue #237: the verbose gate for the client's zero-target op warning.
|
|
89
|
+
# ON by default in dev/test (Rails.env.local?), so the papercut warning
|
|
90
|
+
# works out of the box; production renders no attr and stays silent.
|
|
91
|
+
data[:reactive_verbose] = "true" if Phlex::Reactive.verbose_errors
|
|
88
92
|
# Field-name scope (issue #180): the client prefixes bare binding field
|
|
89
93
|
# names with `scope[...]`. Omitted entirely when undeclared — byte-stable
|
|
90
94
|
# wire for unscoped components.
|
|
@@ -408,6 +412,11 @@ module Phlex
|
|
|
408
412
|
# on()'s flags above.
|
|
409
413
|
attrs[:data][:reactive_outside_param] = "true" if outside
|
|
410
414
|
attrs[:data][:reactive_window_param] = "true" if window_bound
|
|
415
|
+
# Issue #228: mark a clipboard-reading trigger so the client can gate
|
|
416
|
+
# its visibility on availability — on connect the controller sets
|
|
417
|
+
# hidden = !navigator.clipboard.readText. Render the trigger `hidden`
|
|
418
|
+
# for reveal-when-available (a dead paste button never shows).
|
|
419
|
+
attrs[:data][:reactive_clipboard] = "true" if ops.ops.any? { |name, _| name == "paste_into" }
|
|
411
420
|
# Issue #178: confirm: gates the client-op chain behind the SAME
|
|
412
421
|
# overridable confirmResolver on(:action, confirm:) uses (#52/#55). Emits
|
|
413
422
|
# the identical data-reactive-confirm-param; the client's runOps prompts
|
|
@@ -84,7 +84,14 @@ module Phlex
|
|
|
84
84
|
# is the compiled Phlex::Reactive::ParamSchema (issue #109) the endpoint
|
|
85
85
|
# coerces through — built ONCE at declaration so a typo'd type symbol
|
|
86
86
|
# raises Phlex::Reactive::UnknownParamType at class load, not at click time.
|
|
87
|
-
|
|
87
|
+
#
|
|
88
|
+
# Named ActionDefinition (issue #233), NOT Action: this module sits in
|
|
89
|
+
# every reactive component's ancestry, so a bare `Action` constant
|
|
90
|
+
# shadows a host app's Phlex kit component of the same name under lazy
|
|
91
|
+
# autoloading (Kit's LazyLoader resolves through the calling class's
|
|
92
|
+
# const_get). Keep every constant here suffixed / implausible as a kit
|
|
93
|
+
# component name — and never alias the old name back.
|
|
94
|
+
ActionDefinition = Data.define(:name, :params, :schema)
|
|
88
95
|
|
|
89
96
|
# A declared client-side computation (data binding). `inputs`/`outputs` are
|
|
90
97
|
# the action-param names of the fields the reducer reads/writes; `reducer`
|
data/lib/phlex/reactive/js.rb
CHANGED
|
@@ -185,6 +185,35 @@ module Phlex
|
|
|
185
185
|
append("submit", target_args(to, global:))
|
|
186
186
|
end
|
|
187
187
|
|
|
188
|
+
# --- Clipboard-source paste (issue #228) ---
|
|
189
|
+
#
|
|
190
|
+
# paste_into(to) — on a user gesture, read navigator.clipboard.readText()
|
|
191
|
+
# and feed the text into the target field through the normal `input`
|
|
192
|
+
# pipeline: set .value, dispatch a bubbling `input` event (so
|
|
193
|
+
# reactive_compute reducers, reactive_show, and reactive_on_complete all
|
|
194
|
+
# run exactly as if the user had typed it), then focus the field. The
|
|
195
|
+
# permission UX is the browser's own; a rejected or unavailable read is a
|
|
196
|
+
# SILENT NO-OP — page state must not change. The target is a FIELD: there
|
|
197
|
+
# is no :root default and an explicit :root is refused loudly (pasting
|
|
198
|
+
# into the root div is always a call-site bug). ACTOR-ONLY like
|
|
199
|
+
# focus/submit: allowed from on_client / reply.js / reactive_on_complete,
|
|
200
|
+
# refused in broadcast_to(js:) — a broadcast that reads every
|
|
201
|
+
# subscriber's clipboard would be hostile. on_client marks the trigger
|
|
202
|
+
# with data-reactive-clipboard so the client can hide it wherever the
|
|
203
|
+
# clipboard API is missing (no dead button). NOTE: the gate owns the
|
|
204
|
+
# trigger's `hidden` flag — don't also bind reactive_show to the trigger
|
|
205
|
+
# element (the two passes would fight over the same attribute).
|
|
206
|
+
|
|
207
|
+
def paste_into(to, global: false)
|
|
208
|
+
if to == :root
|
|
209
|
+
raise ArgumentError,
|
|
210
|
+
"#{self.class}: paste_into targets a field, not the component root — " \
|
|
211
|
+
"pass the input's CSS selector (e.g. js.paste_into(\"[name=code]\"))"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
append("paste_into", target_args(to, global:))
|
|
215
|
+
end
|
|
216
|
+
|
|
188
217
|
# --- Text content (issue #159) ---
|
|
189
218
|
#
|
|
190
219
|
# text(to, value) — set the target's textContent (stringified; nil clears).
|
|
@@ -314,7 +314,11 @@ data-reactive-flash-level="#{level_attr}"#{dismiss_attr(dismiss_after)}>#{body}<
|
|
|
314
314
|
def js_stream(ops, target: nil)
|
|
315
315
|
json = js_ops_json(ops)
|
|
316
316
|
target_attr = target ? %( target="#{ERB::Util.html_escape(target)}") : ""
|
|
317
|
-
|
|
317
|
+
# Issue #237: carry the verbose gate on the stream element itself, so
|
|
318
|
+
# the client can warn on zero-target resolution even for a
|
|
319
|
+
# document-scoped op with no controller root to read the flag from.
|
|
320
|
+
verbose_attr = Phlex::Reactive.verbose_errors ? %( data-reactive-verbose="true") : ""
|
|
321
|
+
%(<turbo-stream action="reactive:js"#{target_attr}#{verbose_attr} \
|
|
318
322
|
data-reactive-ops="#{ERB::Util.html_escape(json)}"></turbo-stream>).html_safe
|
|
319
323
|
end
|
|
320
324
|
|
|
@@ -121,11 +121,10 @@ module Phlex
|
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
alternatives = negative.flat_map { |field, value| negative_alternatives(field, value) }
|
|
124
|
-
# rubocop:disable Style/ItBlockParameter -- nested block: `it` would shadow `group`
|
|
124
|
+
# rubocop:disable-next Style/ItBlockParameter -- nested block: `it` would shadow `group`
|
|
125
125
|
groups.flat_map do |group|
|
|
126
126
|
alternatives.map { |extra| group + extra }
|
|
127
127
|
end
|
|
128
|
-
# rubocop:enable Style/ItBlockParameter
|
|
129
128
|
end
|
|
130
129
|
|
|
131
130
|
# --- the value language (positive) -------------------------------------
|
|
@@ -35,10 +35,11 @@ module Phlex
|
|
|
35
35
|
|
|
36
36
|
# Actor-only ops: a js broadcast rejects a broadcast that carries one.
|
|
37
37
|
# Focus ops steal focus in every subscriber's tab (issue #96); submit
|
|
38
|
-
# (issue #226) would force-submit every subscriber's form
|
|
38
|
+
# (issue #226) would force-submit every subscriber's form; paste_into
|
|
39
|
+
# (issue #228) would read every subscriber's clipboard. They belong to
|
|
39
40
|
# the actor's own reply (reply.js) or gesture (on_client / a reducer's
|
|
40
41
|
# $ops), never a broadcast. Names mirror Phlex::Reactive::JS's verbs.
|
|
41
|
-
BROADCAST_REFUSED_OPS = %w[focus focus_first submit].freeze
|
|
42
|
+
BROADCAST_REFUSED_OPS = %w[focus focus_first submit paste_into].freeze
|
|
42
43
|
|
|
43
44
|
# The broadcast_to verb kwargs (issue #185) → their Turbo stream action.
|
|
44
45
|
# SELF-TARGETING verbs derive the target from the component's #id (require a
|
|
@@ -124,22 +125,51 @@ module Phlex
|
|
|
124
125
|
"append/prepend/remove), not client-op dispatches"
|
|
125
126
|
end
|
|
126
127
|
resolved_target = resolve_broadcast_target(verb, component, payload, target)
|
|
127
|
-
#
|
|
128
|
-
# class
|
|
129
|
-
#
|
|
130
|
-
#
|
|
128
|
+
# The instrumentation + pgbus thread-locals are owned HERE so the
|
|
129
|
+
# class-level and module-level (plain-component) forms share ONE path
|
|
130
|
+
# and neither is silently un-instrumented (issue #185). The js ops
|
|
131
|
+
# serializer lives HERE too (issue #228): when it was a private method
|
|
132
|
+
# on the includer class, the module-level owner (the Streamable module
|
|
133
|
+
# itself) crashed with NoMethodError instead of refusing — the
|
|
134
|
+
# actor-only gate must be reachable from BOTH broadcast doors.
|
|
131
135
|
component_name = component ? component.class.name : owner.name
|
|
132
136
|
Phlex::Reactive.instrument(
|
|
133
137
|
"broadcast", { component: component_name, stream_action: BROADCAST_VERBS[verb], streamables: keys.size }
|
|
134
138
|
) do
|
|
135
139
|
with_pgbus_broadcast_opts(exclude:, visible_to:) do
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
# A broadcast render NEVER inherits the actor's url_options (issue
|
|
141
|
+
# #232): this call may run inside an action request (where the
|
|
142
|
+
# endpoint threaded the actor's host), but subscribers can be on
|
|
143
|
+
# different hosts — absolute URLs in broadcast-rendered components
|
|
144
|
+
# keep the process defaults, so "host-relative URLs" stays the
|
|
145
|
+
# broadcast contract. Off-request callers pay one nil-write pair.
|
|
146
|
+
html = verb == :js ? nil : Phlex::Reactive.with_url_options(nil) { render_broadcast_html(component) }
|
|
147
|
+
ops_json = verb == :js ? broadcast_js_ops_json(payload) : nil
|
|
138
148
|
keys.each { dispatch_broadcast(verb, it, resolved_target, html, ops_json, morph, effect) }
|
|
139
149
|
end
|
|
140
150
|
end
|
|
141
151
|
end
|
|
142
152
|
|
|
153
|
+
# Validate + serialize broadcast ops: reject actor-only ops (focus steals
|
|
154
|
+
# focus in every tab; submit force-submits every subscriber's form;
|
|
155
|
+
# paste_into reads every subscriber's clipboard) and an empty chain (a
|
|
156
|
+
# dead broadcast), then return the JSON wire form. Works on a JS chain
|
|
157
|
+
# (inspect .ops) and a raw array. Lives on the module singleton — the
|
|
158
|
+
# ONE enforcement point both broadcast doors funnel through.
|
|
159
|
+
def broadcast_js_ops_json(ops)
|
|
160
|
+
pairs = ops.is_a?(Phlex::Reactive::JS) ? ops.ops : Array(ops)
|
|
161
|
+
refused = pairs.map { |name, _| name.to_s } & Phlex::Reactive::Streamable::BROADCAST_REFUSED_OPS
|
|
162
|
+
unless refused.empty?
|
|
163
|
+
raise ArgumentError,
|
|
164
|
+
"broadcast_to(js:) refuses actor-only op(s) #{refused.join(", ")} — broadcasting focus " \
|
|
165
|
+
"steals it in every subscriber's tab; broadcasting submit force-submits every " \
|
|
166
|
+
"subscriber's form; broadcasting paste_into reads every subscriber's clipboard. " \
|
|
167
|
+
"These are actor concerns (reply.js / on_client / $ops)."
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
Phlex::Reactive::Response.js_ops_json(ops)
|
|
171
|
+
end
|
|
172
|
+
|
|
143
173
|
# Render a built component to HTML for a broadcast, ALWAYS instrumented
|
|
144
174
|
# (issue #185): a Streamable renders through its own render_component (which
|
|
145
175
|
# fires render.phlex_reactive); a plain component renders through the module
|
|
@@ -213,8 +243,12 @@ module Phlex
|
|
|
213
243
|
when :prepend then ::Turbo::StreamsChannel.broadcast_prepend_to(*parts, target:, html:, **wire)
|
|
214
244
|
when :remove then ::Turbo::StreamsChannel.broadcast_remove_to(*parts, target:, **wire)
|
|
215
245
|
when :js
|
|
246
|
+
# Issue #237: the broadcast op stream carries the verbose gate too
|
|
247
|
+
# (same server env for every subscriber of this payload).
|
|
248
|
+
data = { reactive_ops: ops_json }
|
|
249
|
+
data[:reactive_verbose] = "true" if Phlex::Reactive.verbose_errors
|
|
216
250
|
::Turbo::StreamsChannel.broadcast_action_to(
|
|
217
|
-
*parts, action: "reactive:js", target:, attributes: { data:
|
|
251
|
+
*parts, action: "reactive:js", target:, attributes: { data: }, render: false
|
|
218
252
|
)
|
|
219
253
|
end
|
|
220
254
|
end
|
|
@@ -463,7 +497,7 @@ module Phlex
|
|
|
463
497
|
# Define the guided-error stub for each removed broadcast method (issue
|
|
464
498
|
# #185). `verb` is referenced in define_method AND the message, so the outer
|
|
465
499
|
# block param must be named — `it` is illegal here.
|
|
466
|
-
# rubocop:disable Style/ItBlockParameter
|
|
500
|
+
# rubocop:disable-next Style/ItBlockParameter
|
|
467
501
|
Phlex::Reactive::Streamable::REMOVED_BROADCASTS.each_key do |verb|
|
|
468
502
|
define_method(verb) do |*, **|
|
|
469
503
|
raise NoMethodError,
|
|
@@ -471,7 +505,6 @@ module Phlex
|
|
|
471
505
|
"use #{name}.#{Phlex::Reactive::Streamable::REMOVED_BROADCASTS[verb]}"
|
|
472
506
|
end
|
|
473
507
|
end
|
|
474
|
-
# rubocop:enable Style/ItBlockParameter
|
|
475
508
|
|
|
476
509
|
private
|
|
477
510
|
|
|
@@ -519,23 +552,6 @@ module Phlex
|
|
|
519
552
|
# instrumentation + pgbus-threading path for the class-level and module-level
|
|
520
553
|
# broadcast_to, so the transport logic has exactly one spelling.)
|
|
521
554
|
|
|
522
|
-
# Validate + serialize broadcast ops: reject actor-only ops (focus steals
|
|
523
|
-
# focus in every tab; submit force-submits every subscriber's form) and an
|
|
524
|
-
# empty chain (a dead broadcast), then return the JSON wire form. Works on
|
|
525
|
-
# a JS chain (inspect .ops) and a raw array.
|
|
526
|
-
def broadcast_js_ops_json(ops)
|
|
527
|
-
pairs = ops.is_a?(Phlex::Reactive::JS) ? ops.ops : Array(ops)
|
|
528
|
-
refused = pairs.map { |name, _| name.to_s } & Phlex::Reactive::Streamable::BROADCAST_REFUSED_OPS
|
|
529
|
-
unless refused.empty?
|
|
530
|
-
raise ArgumentError,
|
|
531
|
-
"broadcast_to(js:) refuses actor-only op(s) #{refused.join(", ")} — broadcasting focus " \
|
|
532
|
-
"steals it in every subscriber's tab; broadcasting submit force-submits every " \
|
|
533
|
-
"subscriber's form. These are actor concerns (reply.js / on_client / $ops)."
|
|
534
|
-
end
|
|
535
|
-
|
|
536
|
-
Phlex::Reactive::Response.js_ops_json(ops)
|
|
537
|
-
end
|
|
538
|
-
|
|
539
555
|
def build(model, options)
|
|
540
556
|
new(**(model ? component_args(model, options) : options))
|
|
541
557
|
end
|
|
@@ -122,7 +122,7 @@ module Phlex
|
|
|
122
122
|
|
|
123
123
|
# does_not_match? is RSpec's REQUIRED negated-matcher protocol method — its
|
|
124
124
|
# name is fixed by RSpec, not a predicate we get to rename.
|
|
125
|
-
# rubocop:disable Naming/PredicatePrefix
|
|
125
|
+
# rubocop:disable-next Naming/PredicatePrefix
|
|
126
126
|
def does_not_match?(page)
|
|
127
127
|
@page = page
|
|
128
128
|
# The negative is satisfied the moment the property is NOT the expected
|
|
@@ -130,7 +130,6 @@ module Phlex
|
|
|
130
130
|
# as that holds, false if it stayed equal for the whole budget.
|
|
131
131
|
poll_until { current_value != @expected }
|
|
132
132
|
end
|
|
133
|
-
# rubocop:enable Naming/PredicatePrefix
|
|
134
133
|
|
|
135
134
|
def failure_message
|
|
136
135
|
"expected ##{@id} to have value #{@expected.inspect} (its .value property), " \
|
data/lib/phlex/reactive.rb
CHANGED
|
@@ -513,6 +513,22 @@ module Phlex
|
|
|
513
513
|
instance = controller_class.new
|
|
514
514
|
instance.set_request!(request)
|
|
515
515
|
instance.set_response!(controller_class.make_response!(request))
|
|
516
|
+
|
|
517
|
+
# Issue #232: during a reactive request the endpoint threads the actor's
|
|
518
|
+
# protocol/host/port via with_url_options; merge them over this
|
|
519
|
+
# instance's defaults so absolute URL helpers in a reply render the
|
|
520
|
+
# REQUESTING host. View contexts delegate url_options to their
|
|
521
|
+
# controller (ActionView::RoutingUrlFor), so this one override covers
|
|
522
|
+
# every helper spelling. Off-request (thread-local nil — jobs, console,
|
|
523
|
+
# broadcasts) returns super's frozen memo untouched: zero-alloc, byte-
|
|
524
|
+
# identical URLs. Defined on the singleton because this instance is
|
|
525
|
+
# MEMOIZED per thread — its request (and super's @_url_options) never
|
|
526
|
+
# changes, so the per-call merge is the only per-request seam.
|
|
527
|
+
def instance.url_options
|
|
528
|
+
overrides = Phlex::Reactive.current_url_options
|
|
529
|
+
overrides ? super.merge(overrides) : super
|
|
530
|
+
end
|
|
531
|
+
|
|
516
532
|
instance.view_context
|
|
517
533
|
end
|
|
518
534
|
|
|
@@ -968,6 +984,40 @@ module Phlex
|
|
|
968
984
|
Thread.current[:phlex_reactive_connection_id] = previous
|
|
969
985
|
end
|
|
970
986
|
|
|
987
|
+
# The acting request's url_options during a reactive request, or nil
|
|
988
|
+
# (issue #232). Set by the ActionsController (the action AND defer
|
|
989
|
+
# endpoints) so absolute URL helpers in a reply-rendered component —
|
|
990
|
+
# image_tag on an Active Storage attachment being the everyday case —
|
|
991
|
+
# carry the REQUESTING host/port/protocol instead of the process default
|
|
992
|
+
# (the ActiveStorage::SetCurrent move). The memoized off-request view
|
|
993
|
+
# context merges this over its defaults per call (see
|
|
994
|
+
# request_bound_view_context); nil (jobs, console, plain broadcasts)
|
|
995
|
+
# leaves the defaults untouched — byte-identical to before.
|
|
996
|
+
def current_url_options
|
|
997
|
+
Thread.current[:phlex_reactive_url_options]
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
# Thread `options` for the block, restoring the previous value after.
|
|
1001
|
+
# `nil` CLEARS the actor options — the broadcast render uses this to keep
|
|
1002
|
+
# a broadcast fired inside an action on process defaults (subscribers can
|
|
1003
|
+
# be on different hosts; "URLs in broadcast-rendered components must be
|
|
1004
|
+
# host-relative" is the broadcast contract).
|
|
1005
|
+
def with_url_options(options)
|
|
1006
|
+
previous = Thread.current[:phlex_reactive_url_options]
|
|
1007
|
+
Thread.current[:phlex_reactive_url_options] = options.presence
|
|
1008
|
+
yield
|
|
1009
|
+
ensure
|
|
1010
|
+
Thread.current[:phlex_reactive_url_options] = previous
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
# The url_options trio derived from a live request. `port` is optional_port
|
|
1014
|
+
# — nil on the default port — and is INCLUDED even when nil so a
|
|
1015
|
+
# default-port request clears any configured default port rather than
|
|
1016
|
+
# inheriting it (the merge must own the whole trio).
|
|
1017
|
+
def url_options_for(request)
|
|
1018
|
+
{ protocol: request.protocol, host: request.host, port: request.optional_port }
|
|
1019
|
+
end
|
|
1020
|
+
|
|
971
1021
|
# The controller a correctly-mounted action path resolves to. Used by the
|
|
972
1022
|
# route guard below.
|
|
973
1023
|
ACTIONS_CONTROLLER = "phlex/reactive/actions"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: phlex-reactive
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.12.
|
|
4
|
+
version: 0.12.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -188,13 +188,13 @@ files:
|
|
|
188
188
|
- lib/phlex/reactive/test_helpers/system.rb
|
|
189
189
|
- lib/phlex/reactive/version.rb
|
|
190
190
|
- lib/tasks/phlex_reactive.rake
|
|
191
|
-
homepage: https://github.com/
|
|
191
|
+
homepage: https://github.com/zoolutions/phlex-reactive
|
|
192
192
|
licenses:
|
|
193
193
|
- MIT
|
|
194
194
|
metadata:
|
|
195
|
-
homepage_uri: https://github.com/
|
|
196
|
-
source_code_uri: https://github.com/
|
|
197
|
-
changelog_uri: https://github.com/
|
|
195
|
+
homepage_uri: https://github.com/zoolutions/phlex-reactive
|
|
196
|
+
source_code_uri: https://github.com/zoolutions/phlex-reactive/tree/main
|
|
197
|
+
changelog_uri: https://github.com/zoolutions/phlex-reactive/blob/main/CHANGELOG.md
|
|
198
198
|
rubygems_mfa_required: 'true'
|
|
199
199
|
rdoc_options: []
|
|
200
200
|
require_paths:
|