phlex-reactive 0.4.7 → 0.9.0

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +970 -0
  3. data/README.md +1134 -49
  4. data/app/controllers/phlex/reactive/actions_controller.rb +282 -190
  5. data/app/javascript/phlex/reactive/compute.js +93 -0
  6. data/app/javascript/phlex/reactive/compute.min.js +4 -0
  7. data/app/javascript/phlex/reactive/compute.min.js.map +10 -0
  8. data/app/javascript/phlex/reactive/confirm.min.js +4 -0
  9. data/app/javascript/phlex/reactive/confirm.min.js.map +10 -0
  10. data/app/javascript/phlex/reactive/reactive_controller.js +1591 -56
  11. data/app/javascript/phlex/reactive/reactive_controller.min.js +4 -0
  12. data/app/javascript/phlex/reactive/reactive_controller.min.js.map +10 -0
  13. data/lib/generators/phlex/reactive/component/USAGE +2 -1
  14. data/lib/generators/phlex/reactive/component/templates/component.rb.erb +1 -2
  15. data/lib/generators/phlex/reactive/component/templates/component_spec.rb.erb +33 -2
  16. data/lib/generators/phlex/reactive/install/install_generator.rb +9 -3
  17. data/lib/generators/phlex/reactive/install/templates/phlex_reactive.rb.erb +37 -0
  18. data/lib/phlex/reactive/component/dsl.rb +249 -0
  19. data/lib/phlex/reactive/component/helpers.rb +595 -0
  20. data/lib/phlex/reactive/component/identity.rb +92 -0
  21. data/lib/phlex/reactive/component/registry.rb +200 -0
  22. data/lib/phlex/reactive/component.rb +37 -348
  23. data/lib/phlex/reactive/doctor.rb +333 -0
  24. data/lib/phlex/reactive/engine.rb +36 -7
  25. data/lib/phlex/reactive/js.rb +222 -0
  26. data/lib/phlex/reactive/log_subscriber.rb +64 -0
  27. data/lib/phlex/reactive/param_schema.rb +390 -0
  28. data/lib/phlex/reactive/reply.rb +5 -3
  29. data/lib/phlex/reactive/response.rb +156 -16
  30. data/lib/phlex/reactive/stream.rb +98 -0
  31. data/lib/phlex/reactive/streamable.rb +307 -43
  32. data/lib/phlex/reactive/test_helpers/matchers.rb +112 -0
  33. data/lib/phlex/reactive/test_helpers.rb +308 -0
  34. data/lib/phlex/reactive/version.rb +1 -1
  35. data/lib/phlex/reactive.rb +329 -14
  36. data/lib/tasks/phlex_reactive.rake +14 -0
  37. metadata +20 -1
@@ -29,13 +29,62 @@ module Phlex
29
29
  class Error < StandardError; end
30
30
 
31
31
  # Raised when a signed identity token fails verification (tampered, expired,
32
- # or signed with a different key).
33
- class InvalidToken < Error; end
32
+ # or signed with a different key) — or when a verified token names a class
33
+ # that doesn't resolve to a reactive component. `diagnostic` classifies the
34
+ # cause (:tampered, :unknown_class, :not_reactive_class) so the endpoint can
35
+ # render a verbose_errors body explaining WHICH failure this was.
36
+ class InvalidToken < Error
37
+ attr_reader :diagnostic
38
+
39
+ def initialize(msg = nil, diagnostic: nil)
40
+ @diagnostic = diagnostic
41
+ super(msg)
42
+ end
43
+ end
44
+
45
+ # Raised at DECLARATION time (Component.action -> ParamSchema.compile) when a
46
+ # param schema names a type symbol that isn't in the registry — a typo like
47
+ # `params: { count: :interger }`. A stdlib ArgumentError subclass (NOT a
48
+ # NameError): the mistake is a bad ARGUMENT to `action`, and callers rescue
49
+ # ArgumentError, not NameError, for a bad-input contract. Loud at class load
50
+ # (eager loading in production; first constant reference under Zeitwerk dev)
51
+ # instead of a silent `to_s` at request time.
52
+ #
53
+ # Superclass is fully qualified `::ArgumentError`: the phlex gem defines
54
+ # Phlex::ArgumentError, and a bare `ArgumentError` here resolves LEXICALLY to
55
+ # that (Phlex::Reactive -> Phlex), coupling us to a Phlex error rather than
56
+ # the stdlib one an app catches.
57
+ class UnknownParamType < ::ArgumentError; end
34
58
 
35
59
  # Purpose string bound into every identity token's signature so a token
36
60
  # minted for phlex-reactive can't be replayed against another verifier use.
37
61
  IDENTITY_PURPOSE = "phlex-reactive/identity"
38
62
 
63
+ # The current identity-token payload version (issue #111), stamped into every
64
+ # signed token under the "v" key. It exists so the NEXT breaking shape change
65
+ # (a rename, per-token expiry, a nonce) can upgrade tokens already in flight
66
+ # instead of breaking every open page at deploy. A token minted before this
67
+ # existed carries no "v" — treated as version 0 (today's shape). Bump this and
68
+ # register a register_token_upgrader(old_version) when you change the shape.
69
+ TOKEN_VERSION = 1
70
+
71
+ # The ActiveSupport::Notifications namespace for the gem's hot-path events
72
+ # (issue #107): action.phlex_reactive, render.phlex_reactive,
73
+ # broadcast.phlex_reactive. APM tools (AppSignal, Datadog, Skylight)
74
+ # auto-subscribe to `*.phlex_reactive` and get component-level visibility.
75
+ # Payloads carry NAMES/outcome/sizes ONLY — never the token, params, or state.
76
+ INSTRUMENTATION_NAMESPACE = "phlex_reactive"
77
+
78
+ # What a component-aware around_action wrapper sees (issue #112). Frozen: a
79
+ # wrapper OBSERVES the resolved action — it must not mutate the context to
80
+ # widen invokability (the token verify, component resolution, default-deny,
81
+ # and schema coercion have already run and are not negotiable here).
82
+ # * component — the resolved, identity-rebuilt component instance
83
+ # * action_name — the declared action Symbol about to run
84
+ # * params — the SCHEMA-COERCED params (dropped keys already gone)
85
+ # * request — the ActionDispatch::Request (remote_ip, headers, ...)
86
+ ActionContext = Data.define(:component, :action_name, :params, :request)
87
+
39
88
  class << self
40
89
  # The message verifier used to sign/verify component identity tokens.
41
90
  # Defaults to a purpose-scoped verifier derived from secret_key_base.
@@ -70,6 +119,162 @@ module Phlex
70
119
  @action_path ||= "/reactive/actions"
71
120
  end
72
121
 
122
+ # Diagnostic endpoint error bodies + dropped-param logging. When true, an
123
+ # endpoint failure (400/403/404) carries a plain-text explanation body
124
+ # (the client already console.errors it) and param coercion warn-logs
125
+ # every dropped key with its bracketed path and reason. Statuses never
126
+ # change with the flag, and the endpoint's warn log fires regardless.
127
+ #
128
+ # Defaults LAZILY to Rails.env.local? — that's development AND test — so
129
+ # production stays opaque unless you opt in. The `defined?` guard (not
130
+ # `||=`) makes an explicit `= false` stick even in dev/test.
131
+ attr_writer :verbose_errors
132
+
133
+ def verbose_errors
134
+ return @verbose_errors if defined?(@verbose_errors)
135
+
136
+ defined?(::Rails.env) && ::Rails.env.local?
137
+ end
138
+
139
+ # Attach the opt-in LogSubscriber (issue #107) so each hot-path event is
140
+ # debug-logged as one compact line (`[reactive] Counter#increment ok
141
+ # (3.1ms)`). Default OFF — the events still fire for APMs regardless; this
142
+ # only controls the gem's own log lines. The engine reads it at boot and
143
+ # attaches exactly once. Set true in an initializer to see the lines.
144
+ attr_writer :log_events
145
+
146
+ def log_events
147
+ return @log_events if defined?(@log_events)
148
+
149
+ false
150
+ end
151
+
152
+ # Client debug mode (issue #108): the "devtools-lite" lens on the reactive
153
+ # round trip. When true, reactive_attrs (and so reactive_root) stamps
154
+ # data-reactive-debug="true" on the root, and the generic controller
155
+ # console.groups every dispatch — action, param/collected NAMES (never
156
+ # values), request encoding, HTTP status, the response's stream actions +
157
+ # targets, whether a token refresh arrived (never the token VALUE), and the
158
+ # round-trip ms. Default OFF (the initializer template suggests
159
+ # Rails.env.development?); zero cost when off (one nil-check per dispatch, no
160
+ # attr, no string building). The `defined?` guard (not `||=`) makes an
161
+ # explicit `= false` stick even where a truthy default might otherwise apply.
162
+ attr_writer :debug
163
+
164
+ def debug
165
+ return @debug if defined?(@debug)
166
+
167
+ false
168
+ end
169
+
170
+ # Register an app-defined param type (issue #109). The block receives the
171
+ # raw client value and returns the coerced value — or Phlex::Reactive::
172
+ # ParamSchema::DROP to reject it (the keyword default then applies, keeping
173
+ # the drop-don't-fabricate contract). Register in an INITIALIZER: the
174
+ # registry is frozen after boot (freeze_param_types!), so a runtime
175
+ # registration raises, and a schema referencing the type is validated at
176
+ # declaration.
177
+ #
178
+ # # config/initializers/phlex_reactive.rb
179
+ # Phlex::Reactive.param_type(:money) do |v|
180
+ # /\A\d+(\.\d{1,2})?\z/.match?(v.to_s) ? BigDecimal(v) : Phlex::Reactive::ParamSchema::DROP
181
+ # end
182
+ # # then: action :charge, params: { amount: :money }
183
+ def param_type(name, &block)
184
+ raise ::ArgumentError, "Phlex::Reactive.param_type requires a block" unless block
185
+
186
+ if param_types_frozen?
187
+ raise Error, "Phlex::Reactive.param_type(#{name.inspect}) called after boot — the param-type " \
188
+ "registry is frozen once the app is initialized. Register custom param types in an " \
189
+ "initializer (config/initializers/phlex_reactive.rb)."
190
+ end
191
+
192
+ param_types[name.to_sym] = block
193
+ end
194
+
195
+ # The param-type registry: Symbol => callable(value) -> coerced | DROP.
196
+ # Seeded lazily with the built-ins; ParamSchema.compile validates every
197
+ # declared type symbol against it, and #coerce dispatches through it.
198
+ def param_types
199
+ @param_types ||= Phlex::Reactive::ParamSchema.built_in_types.dup
200
+ end
201
+
202
+ # A declared type symbol is known iff it's a registry key. ParamSchema
203
+ # asks this at compile so an unknown symbol raises loudly at declaration.
204
+ def param_type?(name)
205
+ param_types.key?(name.to_sym)
206
+ end
207
+
208
+ # Freeze the registry so no further param_type registration is accepted —
209
+ # the initializer-only contract. Called by the engine's after_initialize.
210
+ # Idempotent.
211
+ def freeze_param_types!
212
+ param_types.freeze
213
+ @param_types_frozen = true
214
+ end
215
+
216
+ def param_types_frozen?
217
+ @param_types_frozen ||= false
218
+ end
219
+
220
+ # Drop the registry back to the built-ins and unfreeze it. For tests that
221
+ # register a temporary type; never called in production.
222
+ def reset_param_types!
223
+ @param_types = Phlex::Reactive::ParamSchema.built_in_types.dup
224
+ @param_types_frozen = false
225
+ end
226
+
227
+ # Emit an `<event>.phlex_reactive` ActiveSupport::Notifications event around
228
+ # a block, yielding the mutable payload so a rescue can finalize the outcome
229
+ # (issue #107). ASN.instrument is cheap when nothing is subscribed (the hot
230
+ # paths depend on this — proven by the render bench), so this wraps the
231
+ # render/broadcast/action paths unconditionally. The payload must carry
232
+ # NAMES/outcome/sizes ONLY — never token/params/state.
233
+ def instrument(event, payload = {}, &)
234
+ ::ActiveSupport::Notifications.instrument("#{event}.#{INSTRUMENTATION_NAMESPACE}", payload, &)
235
+ end
236
+
237
+ # Register a COMPONENT-AWARE around_action wrapper (issue #112). The block
238
+ # is folded into the endpoint BETWEEN with_connection_id and the action's
239
+ # transaction — so it sees the resolved component instance, the declared
240
+ # action name, and the coerced params (an ActionContext), and a rejection
241
+ # NEVER opens a transaction. This is the seam for audit logging,
242
+ # component-aware rate limiting, and assertions; the base controller
243
+ # (base_controller_name) remains the seam for HTTP-layer concerns (auth,
244
+ # CSRF, coarse per-IP rate limiting) that don't need the resolved action.
245
+ #
246
+ # Phlex::Reactive.around_action do |ctx, &action|
247
+ # RateLimiter.check!(ctx.request.remote_ip, ctx.action_name) # raise -> 403
248
+ # result = action.call
249
+ # AuditLog.record!(actor: Current.user, action: ctx.action_name)
250
+ # result # <- REQUIRED: return the continuation's value
251
+ # end
252
+ #
253
+ # CONTRACT — each wrapper MUST return `action.call`'s value. The endpoint
254
+ # type-checks the action's return for a Phlex::Reactive::Response; a wrapper
255
+ # that returns its logger's result instead silently downgrades every reply
256
+ # to the implicit self-replace. Wrappers nest in registration order with the
257
+ # LAST-registered outermost. Register in an initializer.
258
+ def around_action(&block)
259
+ raise ::ArgumentError, "Phlex::Reactive.around_action requires a block" unless block
260
+
261
+ around_actions << block
262
+ end
263
+
264
+ # The registered around_action wrappers, oldest first. The endpoint folds
265
+ # them so the last-registered runs outermost; an empty stack is the default
266
+ # hot path (the controller short-circuits with `return yield`).
267
+ def around_actions
268
+ @around_actions ||= []
269
+ end
270
+
271
+ # Drop all registered around_action wrappers. For test isolation (the
272
+ # shipped hook — specs registering a temporary wrapper reset around every
273
+ # example); never called in production.
274
+ def reset_around_actions!
275
+ @around_actions = []
276
+ end
277
+
73
278
  def verifier
74
279
  @verifier ||= default_verifier
75
280
  end
@@ -110,6 +315,24 @@ module Phlex
110
315
 
111
316
  attr_writer :flash_target
112
317
 
318
+ # A user-visible flash rendered on every endpoint rescue path (issue #100).
319
+ # Default nil = today's behavior (a bare head, or the verbose_errors
320
+ # plain-text diagnostic). Set a lambda ->(kind) { "message" } (kind is
321
+ # :tampered/:unknown_class/:not_reactive_class/:forbidden/:not_found) and
322
+ # the ActionsController ALSO renders a turbo-stream flash into flash_target
323
+ # — at the SAME status it returns today (statuses never change). Composes
324
+ # with verbose_errors: the turbo-stream flash wins the response body, the
325
+ # diagnostic still goes to the log.
326
+ attr_accessor :error_flash
327
+
328
+ # Component class used to render STRING flash content (issue #77).
329
+ # Instantiated as flash_component.new(level:, content:) and rendered
330
+ # through the existing render path, replacing the built-in
331
+ # <div class="reactive-flash reactive-flash--{level}"> wrapper. Default
332
+ # nil (the built-in wrapper). Phlex component content passed to
333
+ # reply.flash always renders verbatim and bypasses this.
334
+ attr_accessor :flash_component
335
+
113
336
  # Render a Phlex component to HTML with a full (off-request) view context.
114
337
  # Uses phlex-rails' #render_in against the memoized view context — a direct
115
338
  # component.call that skips ActionController's renderer.render machinery
@@ -121,20 +344,24 @@ module Phlex
121
344
  end
122
345
 
123
346
  # A Turbo::Streams::TagBuilder bound to an off-request view context, used
124
- # to build standalone streams (e.g. a Response flash append) not tied to a
125
- # specific component's id. Cached PER THREAD alongside the context it's
126
- # bound to (see off_request_view_context for why per-thread).
127
- def flash_builder
347
+ # to build standalone streams not tied to a specific component's id a
348
+ # Response flash append, a reactive_collection row removal, a count
349
+ # companion update, an also_update companion. Cached PER THREAD alongside
350
+ # the context it's bound to (see off_request_view_context for why
351
+ # per-thread). Renamed from flash_builder (issue #113): the builder does
352
+ # far more than flashes, so the name misled. flash_builder stays a
353
+ # permanent alias below so the engine's to_prepare and app code keep working.
354
+ def stream_builder
128
355
  off_request_view_context_cache[:builder]
129
356
  end
130
357
 
131
358
  # The off-request view context for the current thread, built once and
132
- # reused for both the flash builder and standalone component renders.
359
+ # reused for both the stream builder and standalone component renders.
133
360
  # Cached PER THREAD, not per process: an ActionView context carries mutable
134
361
  # output_buffer/view_flow state (render_in's capture swaps it), so sharing
135
362
  # one instance across threads can interleave content on a threaded server.
136
363
  # Rebuilt when the renderer object changes or the generation is bumped
137
- # (reset_flash_builder! / Rails code reload), so a reloaded controller is
364
+ # (reset_stream_builder! / Rails code reload), so a reloaded controller is
138
365
  # never served stale.
139
366
  def off_request_view_context
140
367
  off_request_view_context_cache[:view_context]
@@ -143,11 +370,19 @@ module Phlex
143
370
  # Invalidate the per-thread context + builder for ALL threads by bumping
144
371
  # the generation; each thread rebuilds lazily on next use. Registered on
145
372
  # Rails' reloader by the engine; also used by specs. Thread-safe (an
146
- # integer bump, no shared structure to tear down).
147
- def reset_flash_builder!
373
+ # integer bump, no shared structure to tear down). Renamed from
374
+ # reset_flash_builder! (issue #113); the old name stays a permanent alias
375
+ # below so the engine's to_prepare hook keeps working.
376
+ def reset_stream_builder!
148
377
  @off_request_view_context_generation = off_request_view_context_generation + 1
149
378
  end
150
379
 
380
+ # Permanent aliases for the pre-#113 names. Kept forever (no deprecation)
381
+ # so the engine's to_prepare hook and any app code calling the old names
382
+ # keep working unchanged.
383
+ alias flash_builder stream_builder
384
+ alias reset_flash_builder! reset_stream_builder!
385
+
151
386
  def off_request_view_context_generation
152
387
  @off_request_view_context_generation ||= 0
153
388
  end
@@ -182,14 +417,65 @@ module Phlex
182
417
  base_controller_name.constantize
183
418
  end
184
419
 
185
- # Returns the verified payload hash, or nil if the token is invalid.
420
+ # Returns the verified, version-upgraded payload hash, or nil if the token
421
+ # is invalid (bad signature/purpose) OR carries a version this code doesn't
422
+ # understand. The single verify choke point (issue #111): after the
423
+ # signature check we run upgrade_token so an older-shape payload is migrated
424
+ # to the current shape before from_identity ever sees it.
186
425
  def verify(token)
187
- verifier.verified(token, purpose: IDENTITY_PURPOSE)
426
+ payload = verifier.verified(token, purpose: IDENTITY_PURPOSE)
427
+ payload && upgrade_token(payload)
188
428
  end
189
429
 
190
- # Signs a payload hash into an identity token.
430
+ # Signs a payload hash into an identity token, stamping the current
431
+ # TOKEN_VERSION (issue #111). The "v" key is the ONLY thing added — a
432
+ # from_identity that ignores unknown keys is unaffected.
191
433
  def sign(payload)
192
- verifier.generate(payload, purpose: IDENTITY_PURPOSE)
434
+ verifier.generate(payload.merge("v" => TOKEN_VERSION), purpose: IDENTITY_PURPOSE)
435
+ end
436
+
437
+ # Migrate a verified payload from whatever version it was signed at up to
438
+ # TOKEN_VERSION (issue #111). Runs the registered upgraders oldest → current,
439
+ # then stamps the payload to the current version. Contract:
440
+ # * no "v" → version 0 (the shape from before versioning existed). With no
441
+ # v0 upgrader registered this is a pure passthrough — introducing
442
+ # versioning invalidates NOTHING already in flight.
443
+ # * v == current → returned as-is (the hot path — one integer compare).
444
+ # * v > current → nil. A rolled-back deploy verifying a token minted by
445
+ # NEWER code must not guess the newer shape; returning nil fails closed
446
+ # through the endpoint's existing `|| raise(InvalidToken)` → 400.
447
+ def upgrade_token(payload)
448
+ version = payload.fetch("v", 0)
449
+ return payload if version == TOKEN_VERSION
450
+ return nil if version > TOKEN_VERSION
451
+
452
+ upgrade_from(payload, version)
453
+ end
454
+
455
+ # Register an upgrader that rewrites a payload signed at `from_version` into
456
+ # the shape of `from_version + 1` (issue #111). Register in load order at
457
+ # boot when you bump TOKEN_VERSION; the block receives the payload hash and
458
+ # returns the migrated hash (the "v" stamp is applied by upgrade_token, so
459
+ # the block only reshapes the data). Example, for a future count → n rename:
460
+ #
461
+ # Phlex::Reactive.register_token_upgrader(0) do |payload|
462
+ # payload.merge("s" => { "n" => payload.dig("s", "count") })
463
+ # end
464
+ def register_token_upgrader(from_version, &block)
465
+ raise ::ArgumentError, "register_token_upgrader requires a block" unless block
466
+
467
+ token_upgraders[Integer(from_version)] = block
468
+ end
469
+
470
+ # from_version => callable(payload) -> migrated payload. Sparse: an entry
471
+ # exists only for a version that actually changed shape.
472
+ def token_upgraders
473
+ @token_upgraders ||= {}
474
+ end
475
+
476
+ # Drop all registered upgraders. For tests that register a temporary one.
477
+ def reset_token_upgraders!
478
+ @token_upgraders = {}
193
479
  end
194
480
 
195
481
  # The acting client's SSE connection id during an action, or nil. Set by
@@ -265,6 +551,27 @@ module Phlex
265
551
  ::Rails.logger if defined?(::Rails) && ::Rails.respond_to?(:logger)
266
552
  end
267
553
 
554
+ # Walk the upgrader chain from `version` up to TOKEN_VERSION, applying each
555
+ # registered upgrader in turn (issue #111). A gap in the chain (a version
556
+ # bumped with no shape change, so no upgrader registered) is a no-op step —
557
+ # the payload passes through unchanged to the next version. Only stamp the
558
+ # current "v" if an upgrader ACTUALLY reshaped the payload: a versionless
559
+ # (v0) token with no upgraders registered — today's case — is returned
560
+ # byte-identical, so introducing versioning invalidates nothing in flight.
561
+ # Runs only for a genuinely old token (the v == current hot path already
562
+ # returned in upgrade_token), so this is off the render path.
563
+ def upgrade_from(payload, version)
564
+ upgraded = false
565
+ version.upto(TOKEN_VERSION - 1) do
566
+ upgrader = token_upgraders[it]
567
+ next unless upgrader
568
+
569
+ payload = upgrader.call(payload)
570
+ upgraded = true
571
+ end
572
+ upgraded ? payload.merge("v" => TOKEN_VERSION) : payload
573
+ end
574
+
268
575
  def default_verifier
269
576
  unless defined?(::Rails) && ::Rails.application
270
577
  raise Error, "Phlex::Reactive.verifier is unset and Rails.application is unavailable; " \
@@ -285,6 +592,9 @@ loader.tag = "phlex-reactive"
285
592
  # (lib/phlex/reactive/foo.rb -> Phlex::Reactive::Foo).
286
593
  lib = File.expand_path("..", __dir__)
287
594
  loader.push_dir(lib)
595
+ # js.rb defines JS and component/dsl.rb defines Component::DSL (acronyms), not
596
+ # the default-inflected `Js`/`Dsl`.
597
+ loader.inflector.inflect("js" => "JS", "dsl" => "DSL")
288
598
  # The gem-name shim (`require "phlex-reactive"`) is a plain require, not a
289
599
  # managed file.
290
600
  loader.ignore("#{lib}/phlex-reactive.rb")
@@ -296,6 +606,11 @@ loader.ignore("#{lib}/phlex/reactive/version.rb")
296
606
  # (generators/phlex/reactive/... defining Phlex::Reactive::Generators::...)
297
607
  # deliberately doesn't follow Zeitwerk's rules, so the loader must ignore them.
298
608
  loader.ignore("#{lib}/generators")
609
+ # The RSpec matchers file (issue #110) defines RSpec::Matchers, not a
610
+ # Phlex::Reactive::TestHelpers::Matchers constant, so it doesn't follow
611
+ # Zeitwerk's naming — test_helpers.rb requires it explicitly (only when RSpec is
612
+ # present), and the loader must ignore it.
613
+ loader.ignore("#{lib}/phlex/reactive/test_helpers/matchers.rb")
299
614
  # The engine is required explicitly below (only when Rails is present) and must
300
615
  # not be eager-loaded before that.
301
616
  loader.do_not_eager_load("#{__dir__}/reactive/engine.rb")
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Rails engines auto-load rake tasks under lib/tasks — no engine.rb change is
4
+ # needed for `bin/rails phlex_reactive:doctor` to appear in the host app.
5
+ namespace :phlex_reactive do
6
+ desc "Validate the phlex-reactive install (route, Stimulus, verifier, components) — issue #106"
7
+ task doctor: :environment do
8
+ # Doctor.run eager-loads so the component registry is populated, prints the
9
+ # ✓/✗/? report (with a fix per failure), and returns false if any check
10
+ # FAILED (advisory `?` lines don't count) — abort so CI / a setup script can
11
+ # gate on `bin/rails phlex_reactive:doctor`.
12
+ abort unless Phlex::Reactive::Doctor.run
13
+ end
14
+ end
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.4.7
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -117,8 +117,15 @@ files:
117
117
  - LICENSE.txt
118
118
  - README.md
119
119
  - app/controllers/phlex/reactive/actions_controller.rb
120
+ - app/javascript/phlex/reactive/compute.js
121
+ - app/javascript/phlex/reactive/compute.min.js
122
+ - app/javascript/phlex/reactive/compute.min.js.map
120
123
  - app/javascript/phlex/reactive/confirm.js
124
+ - app/javascript/phlex/reactive/confirm.min.js
125
+ - app/javascript/phlex/reactive/confirm.min.js.map
121
126
  - app/javascript/phlex/reactive/reactive_controller.js
127
+ - app/javascript/phlex/reactive/reactive_controller.min.js
128
+ - app/javascript/phlex/reactive/reactive_controller.min.js.map
122
129
  - lib/generators/phlex/reactive/component/USAGE
123
130
  - lib/generators/phlex/reactive/component/component_generator.rb
124
131
  - lib/generators/phlex/reactive/component/templates/component.rb.erb
@@ -129,11 +136,23 @@ files:
129
136
  - lib/phlex-reactive.rb
130
137
  - lib/phlex/reactive.rb
131
138
  - lib/phlex/reactive/component.rb
139
+ - lib/phlex/reactive/component/dsl.rb
140
+ - lib/phlex/reactive/component/helpers.rb
141
+ - lib/phlex/reactive/component/identity.rb
142
+ - lib/phlex/reactive/component/registry.rb
143
+ - lib/phlex/reactive/doctor.rb
132
144
  - lib/phlex/reactive/engine.rb
145
+ - lib/phlex/reactive/js.rb
146
+ - lib/phlex/reactive/log_subscriber.rb
147
+ - lib/phlex/reactive/param_schema.rb
133
148
  - lib/phlex/reactive/reply.rb
134
149
  - lib/phlex/reactive/response.rb
150
+ - lib/phlex/reactive/stream.rb
135
151
  - lib/phlex/reactive/streamable.rb
152
+ - lib/phlex/reactive/test_helpers.rb
153
+ - lib/phlex/reactive/test_helpers/matchers.rb
136
154
  - lib/phlex/reactive/version.rb
155
+ - lib/tasks/phlex_reactive.rake
137
156
  homepage: https://github.com/mhenrixon/phlex-reactive
138
157
  licenses:
139
158
  - MIT