phlex-reactive 0.12.4 → 0.12.6
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 +82 -0
- data/README.md +104 -2
- data/app/controllers/phlex/reactive/actions_controller.rb +19 -6
- data/app/javascript/phlex/reactive/reactive_controller.js +473 -10
- 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 +89 -0
- data/lib/phlex/reactive/component.rb +8 -1
- data/lib/phlex/reactive/js.rb +30 -0
- data/lib/phlex/reactive/response.rb +5 -1
- data/lib/phlex/reactive/show_conditions.rb +1 -2
- data/lib/phlex/reactive/streamable.rb +14 -5
- 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.
|
|
@@ -1080,6 +1084,91 @@ module Phlex
|
|
|
1080
1084
|
{ data: { reactive_show_targets: normalized.to_json } }
|
|
1081
1085
|
end
|
|
1082
1086
|
|
|
1087
|
+
# Client-only localStorage draft over the fields this root OWNS (issue
|
|
1088
|
+
# #239) — "don't make me start over". Spread on the ROOT (mix with
|
|
1089
|
+
# reactive_root, like reactive_show_targets); the generic controller
|
|
1090
|
+
# then writes every owned control's value to localStorage as the user
|
|
1091
|
+
# types (debounced on `input`, immediate on `change`, flushed on
|
|
1092
|
+
# disconnect), restores the draft on the NEXT connect, and forgets it
|
|
1093
|
+
# when the owning form submits successfully (turbo:submit-end), when
|
|
1094
|
+
# `ttl` elapses, or on a js.persist_clear op:
|
|
1095
|
+
#
|
|
1096
|
+
# div(**mix(reactive_root, reactive_persist(key: "village-apply", ttl: 7.days))) do
|
|
1097
|
+
# input(**reactive_field(:name)) # persisted
|
|
1098
|
+
# input(name: "fuckery", **reactive_persist_skip) # honeypot — never
|
|
1099
|
+
# input(type: "hidden", name: "tz") # hidden — never (default)
|
|
1100
|
+
# end
|
|
1101
|
+
#
|
|
1102
|
+
# Never persisted: type=hidden/file/password/submit/button/reset/image,
|
|
1103
|
+
# anything carrying reactive_persist_skip, a nested reactive root's
|
|
1104
|
+
# controls (#15 ownership), and — when `fields:` narrows the set —
|
|
1105
|
+
# any name outside it (scope-aware symbols, the reactive_show form).
|
|
1106
|
+
# `autocomplete="off"` is NOT an implicit skip: honeypots (an
|
|
1107
|
+
# invisible_captcha text input looks like any other) must opt out
|
|
1108
|
+
# explicitly or sit outside the root.
|
|
1109
|
+
#
|
|
1110
|
+
# `restore:` — `:blank` (default) restores a draft value only into a
|
|
1111
|
+
# control the server rendered BLANK, so a 422 re-render's submitted
|
|
1112
|
+
# values win over an older draft; `:always` lets the draft win.
|
|
1113
|
+
#
|
|
1114
|
+
# Same posture as reactive_show: no token, no POST, no expression
|
|
1115
|
+
# surface. Stored values are plain user input replayed via
|
|
1116
|
+
# .value/.checked (never HTML) — a tampered draft can only fill what
|
|
1117
|
+
# the user could type. PII sits in localStorage for `ttl`; the
|
|
1118
|
+
# successful-submit clear and `ttl` are the shared-computer mitigation
|
|
1119
|
+
# (see docs/security). ONE call per root — Phlex `mix` space-joins
|
|
1120
|
+
# duplicate string data values, so a second call would corrupt the JSON.
|
|
1121
|
+
def reactive_persist(key:, ttl: 7.days, fields: nil, restore: :blank, debounce: 300)
|
|
1122
|
+
payload = {
|
|
1123
|
+
"key" => validate_persist_key!(key),
|
|
1124
|
+
"ttl" => validate_persist_ttl!(ttl),
|
|
1125
|
+
"debounce" => validate_persist_debounce!(debounce)
|
|
1126
|
+
}
|
|
1127
|
+
payload["restore"] = "always" if validate_persist_restore!(restore) == :always
|
|
1128
|
+
payload["fields"] = validate_persist_fields!(fields) if fields
|
|
1129
|
+
|
|
1130
|
+
{ data: { reactive_persist: payload.to_json } }
|
|
1131
|
+
end
|
|
1132
|
+
|
|
1133
|
+
# Mark ONE control as never persisted (issue #239) — a honeypot, a
|
|
1134
|
+
# one-time code. Spread on the control: input(name: "x", **reactive_persist_skip).
|
|
1135
|
+
def reactive_persist_skip
|
|
1136
|
+
{ data: { reactive_persist: "off" } }
|
|
1137
|
+
end
|
|
1138
|
+
|
|
1139
|
+
def validate_persist_key!(key)
|
|
1140
|
+
return key if key.is_a?(String) && !key.strip.empty?
|
|
1141
|
+
|
|
1142
|
+
raise ArgumentError, "reactive_persist key: must be a non-blank String, got #{key.inspect}"
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
# Seconds on the wire: an ActiveSupport::Duration (7.days) or an Integer.
|
|
1146
|
+
def validate_persist_ttl!(ttl)
|
|
1147
|
+
seconds = ttl.is_a?(ActiveSupport::Duration) ? ttl.to_i : ttl
|
|
1148
|
+
return seconds if seconds.is_a?(Integer) && seconds.positive?
|
|
1149
|
+
|
|
1150
|
+
raise ArgumentError, "reactive_persist ttl: must be a positive duration or Integer seconds, got #{ttl.inspect}"
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
def validate_persist_debounce!(debounce)
|
|
1154
|
+
return debounce if debounce.is_a?(Integer) && !debounce.negative?
|
|
1155
|
+
|
|
1156
|
+
raise ArgumentError, "reactive_persist debounce: must be a non-negative Integer (ms), got #{debounce.inspect}"
|
|
1157
|
+
end
|
|
1158
|
+
|
|
1159
|
+
def validate_persist_restore!(restore)
|
|
1160
|
+
return restore if %i[blank always].include?(restore)
|
|
1161
|
+
|
|
1162
|
+
raise ArgumentError, "reactive_persist restore: must be :blank or :always, got #{restore.inspect}"
|
|
1163
|
+
end
|
|
1164
|
+
|
|
1165
|
+
def validate_persist_fields!(fields)
|
|
1166
|
+
list = Array(fields)
|
|
1167
|
+
raise ArgumentError, "reactive_persist fields: needs at least one field name" if list.empty?
|
|
1168
|
+
|
|
1169
|
+
list.map { scoped_field_name(it) }
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1083
1172
|
# Scoped busy indicator (issue #99). Marks an element so the generic
|
|
1084
1173
|
# controller toggles `data-reactive-busy` on it ONLY while `action` is in
|
|
1085
1174
|
# flight — the scoped sibling of the always-on `data-reactive-busy` the
|
|
@@ -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
|
@@ -252,6 +252,36 @@ module Phlex
|
|
|
252
252
|
@ops.empty?
|
|
253
253
|
end
|
|
254
254
|
|
|
255
|
+
# --- Client-only drafts (issue #239) ---
|
|
256
|
+
#
|
|
257
|
+
# persist_state(**state) — merge a FLAT bag of scalars (a wizard's
|
|
258
|
+
# current step) into the root's reactive_persist draft alongside the
|
|
259
|
+
# owned fields' values; the client restores it on the next connect as
|
|
260
|
+
# data-reactive-persist-state on the root plus the
|
|
261
|
+
# reactive:persist-restored event's detail.state. Always targets the
|
|
262
|
+
# ROOT (the draft lives there). persist_clear — forget the draft now
|
|
263
|
+
# (the explicit sibling of the automatic turbo:submit-end clear).
|
|
264
|
+
# Both ACTOR-ONLY like focus/submit: a broadcast that rewrote or wiped
|
|
265
|
+
# every subscriber's draft would be hostile (BROADCAST_REFUSED_OPS).
|
|
266
|
+
|
|
267
|
+
def persist_state(**state)
|
|
268
|
+
raise ArgumentError, "#{self.class}: persist_state needs at least one key (e.g. step: 2)" if state.empty?
|
|
269
|
+
|
|
270
|
+
state.each do |name, value|
|
|
271
|
+
next if value.nil? || [String, Numeric, TrueClass, FalseClass].any? { value.is_a?(it) }
|
|
272
|
+
|
|
273
|
+
raise ArgumentError,
|
|
274
|
+
"#{self.class}: persist_state values must be scalar (String/Numeric/true/false/nil) — " \
|
|
275
|
+
"#{name.inspect} is #{value.class} (the draft stays flat)"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
append("persist_state", { "to" => ROOT_SENTINEL, "state" => state.transform_keys(&:to_s).freeze }.freeze)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def persist_clear
|
|
282
|
+
append("persist_clear", { "to" => ROOT_SENTINEL }.freeze)
|
|
283
|
+
end
|
|
284
|
+
|
|
255
285
|
private
|
|
256
286
|
|
|
257
287
|
# Immutability: every verb funnels here and returns a NEW frozen chain —
|
|
@@ -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) -------------------------------------
|
|
@@ -39,7 +39,7 @@ module Phlex
|
|
|
39
39
|
# (issue #228) would read every subscriber's clipboard. They belong to
|
|
40
40
|
# the actor's own reply (reply.js) or gesture (on_client / a reducer's
|
|
41
41
|
# $ops), never a broadcast. Names mirror Phlex::Reactive::JS's verbs.
|
|
42
|
-
BROADCAST_REFUSED_OPS = %w[focus focus_first submit paste_into].freeze
|
|
42
|
+
BROADCAST_REFUSED_OPS = %w[focus focus_first submit paste_into persist_state persist_clear].freeze
|
|
43
43
|
|
|
44
44
|
# The broadcast_to verb kwargs (issue #185) → their Turbo stream action.
|
|
45
45
|
# SELF-TARGETING verbs derive the target from the component's #id (require a
|
|
@@ -137,7 +137,13 @@ module Phlex
|
|
|
137
137
|
"broadcast", { component: component_name, stream_action: BROADCAST_VERBS[verb], streamables: keys.size }
|
|
138
138
|
) do
|
|
139
139
|
with_pgbus_broadcast_opts(exclude:, visible_to:) do
|
|
140
|
-
|
|
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) }
|
|
141
147
|
ops_json = verb == :js ? broadcast_js_ops_json(payload) : nil
|
|
142
148
|
keys.each { dispatch_broadcast(verb, it, resolved_target, html, ops_json, morph, effect) }
|
|
143
149
|
end
|
|
@@ -237,8 +243,12 @@ module Phlex
|
|
|
237
243
|
when :prepend then ::Turbo::StreamsChannel.broadcast_prepend_to(*parts, target:, html:, **wire)
|
|
238
244
|
when :remove then ::Turbo::StreamsChannel.broadcast_remove_to(*parts, target:, **wire)
|
|
239
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
|
|
240
250
|
::Turbo::StreamsChannel.broadcast_action_to(
|
|
241
|
-
*parts, action: "reactive:js", target:, attributes: { data:
|
|
251
|
+
*parts, action: "reactive:js", target:, attributes: { data: }, render: false
|
|
242
252
|
)
|
|
243
253
|
end
|
|
244
254
|
end
|
|
@@ -487,7 +497,7 @@ module Phlex
|
|
|
487
497
|
# Define the guided-error stub for each removed broadcast method (issue
|
|
488
498
|
# #185). `verb` is referenced in define_method AND the message, so the outer
|
|
489
499
|
# block param must be named — `it` is illegal here.
|
|
490
|
-
# rubocop:disable Style/ItBlockParameter
|
|
500
|
+
# rubocop:disable-next Style/ItBlockParameter
|
|
491
501
|
Phlex::Reactive::Streamable::REMOVED_BROADCASTS.each_key do |verb|
|
|
492
502
|
define_method(verb) do |*, **|
|
|
493
503
|
raise NoMethodError,
|
|
@@ -495,7 +505,6 @@ module Phlex
|
|
|
495
505
|
"use #{name}.#{Phlex::Reactive::Streamable::REMOVED_BROADCASTS[verb]}"
|
|
496
506
|
end
|
|
497
507
|
end
|
|
498
|
-
# rubocop:enable Style/ItBlockParameter
|
|
499
508
|
|
|
500
509
|
private
|
|
501
510
|
|
|
@@ -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.6
|
|
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:
|