ruact 0.0.10 → 0.0.12

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +123 -48
  3. data/CONTRIBUTING.md +233 -0
  4. data/README.md +16 -6
  5. data/RELEASING.md +125 -139
  6. data/lib/ruact/packaging.rb +68 -0
  7. data/lib/ruact/version.rb +1 -1
  8. data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +321 -0
  9. data/vendor/javascript/vite-plugin-ruact/package-lock.json +11 -0
  10. data/vendor/javascript/vite-plugin-ruact/package.json +1 -0
  11. data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +14 -3
  12. metadata +6 -107
  13. data/.codecov.yml +0 -31
  14. data/.github/workflows/ci.yml +0 -284
  15. data/.github/workflows/server-functions-bench.yml +0 -54
  16. data/.rubocop.yml +0 -107
  17. data/.rubocop_todo.yml +0 -63
  18. data/Rakefile +0 -10
  19. data/bench/server_functions_dispatch_bench.rb +0 -276
  20. data/bench/server_functions_dispatch_bench.results.md +0 -150
  21. data/docs/internal/README.md +0 -9
  22. data/docs/internal/decisions/server-functions-api.md +0 -2236
  23. data/spec/benchmarks/baseline.json +0 -1
  24. data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -92
  25. data/spec/fixtures/flight/README.md +0 -136
  26. data/spec/fixtures/flight/array.txt +0 -1
  27. data/spec/fixtures/flight/as_json_object.txt +0 -2
  28. data/spec/fixtures/flight/bigint.txt +0 -1
  29. data/spec/fixtures/flight/boolean_false.txt +0 -1
  30. data/spec/fixtures/flight/boolean_true.txt +0 -1
  31. data/spec/fixtures/flight/client_component_with_props.txt +0 -2
  32. data/spec/fixtures/flight/client_reference.txt +0 -2
  33. data/spec/fixtures/flight/hash.txt +0 -1
  34. data/spec/fixtures/flight/infinity.txt +0 -1
  35. data/spec/fixtures/flight/nan.txt +0 -1
  36. data/spec/fixtures/flight/negative_infinity.txt +0 -1
  37. data/spec/fixtures/flight/nil.txt +0 -1
  38. data/spec/fixtures/flight/number_float.txt +0 -1
  39. data/spec/fixtures/flight/number_integer.txt +0 -1
  40. data/spec/fixtures/flight/react_element_no_props.txt +0 -1
  41. data/spec/fixtures/flight/redirect_row.txt +0 -1
  42. data/spec/fixtures/flight/serializable_object.txt +0 -2
  43. data/spec/fixtures/flight/string_basic.txt +0 -1
  44. data/spec/fixtures/flight/string_dollar_escape.txt +0 -1
  45. data/spec/fixtures/flight/undefined.txt +0 -1
  46. data/spec/fixtures/readme/children-error.html.erb +0 -3
  47. data/spec/fixtures/readme/children-error.txt +0 -1
  48. data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +0 -3
  49. data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +0 -3
  50. data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +0 -3
  51. data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +0 -3
  52. data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +0 -3
  53. data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +0 -3
  54. data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +0 -3
  55. data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +0 -16
  56. data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +0 -24
  57. data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +0 -15
  58. data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +0 -17
  59. data/spec/readme_demo_message_spec.rb +0 -67
  60. data/spec/readme_spec.rb +0 -282
  61. data/spec/ruact/client_manifest_spec.rb +0 -270
  62. data/spec/ruact/component_contract_spec.rb +0 -119
  63. data/spec/ruact/configuration_spec.rb +0 -518
  64. data/spec/ruact/controller_request_spec.rb +0 -671
  65. data/spec/ruact/controller_spec.rb +0 -343
  66. data/spec/ruact/doctor_spec.rb +0 -769
  67. data/spec/ruact/erb_preprocessor_hook_spec.rb +0 -55
  68. data/spec/ruact/erb_preprocessor_spec.rb +0 -361
  69. data/spec/ruact/errors_spec.rb +0 -93
  70. data/spec/ruact/flight/renderer_spec.rb +0 -133
  71. data/spec/ruact/flight/serializer_spec.rb +0 -494
  72. data/spec/ruact/html_converter_spec.rb +0 -375
  73. data/spec/ruact/install_generator_spec.rb +0 -1549
  74. data/spec/ruact/layout_source_spec.rb +0 -108
  75. data/spec/ruact/manifest_resolver_spec.rb +0 -174
  76. data/spec/ruact/query_request_spec.rb +0 -706
  77. data/spec/ruact/query_spec.rb +0 -105
  78. data/spec/ruact/railtie_spec.rb +0 -155
  79. data/spec/ruact/render_context_spec.rb +0 -58
  80. data/spec/ruact/render_pipeline_concurrency_spec.rb +0 -78
  81. data/spec/ruact/render_pipeline_spec.rb +0 -928
  82. data/spec/ruact/scaffold_generator_spec.rb +0 -1849
  83. data/spec/ruact/serializable_spec.rb +0 -179
  84. data/spec/ruact/server_bucket_request_spec.rb +0 -785
  85. data/spec/ruact/server_function_name_spec.rb +0 -53
  86. data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +0 -63
  87. data/spec/ruact/server_functions/bucket_two_payload_spec.rb +0 -200
  88. data/spec/ruact/server_functions/codegen_spec.rb +0 -397
  89. data/spec/ruact/server_functions/error_payload_spec.rb +0 -222
  90. data/spec/ruact/server_functions/error_suggestion_spec.rb +0 -79
  91. data/spec/ruact/server_functions/introspection_spec.rb +0 -135
  92. data/spec/ruact/server_functions/name_bridge_spec.rb +0 -212
  93. data/spec/ruact/server_functions/query_context_spec.rb +0 -72
  94. data/spec/ruact/server_functions/query_source_spec.rb +0 -193
  95. data/spec/ruact/server_functions/railtie_integration_spec.rb +0 -215
  96. data/spec/ruact/server_functions/rake_spec.rb +0 -86
  97. data/spec/ruact/server_functions/route_source_spec.rb +0 -202
  98. data/spec/ruact/server_functions/snapshot_spec.rb +0 -96
  99. data/spec/ruact/server_functions/snapshot_writer_spec.rb +0 -71
  100. data/spec/ruact/server_rescue_request_spec.rb +0 -416
  101. data/spec/ruact/server_spec.rb +0 -179
  102. data/spec/ruact/server_upload_request_spec.rb +0 -311
  103. data/spec/ruact/signed_references_spec.rb +0 -164
  104. data/spec/ruact/string_distance_spec.rb +0 -38
  105. data/spec/ruact/tasks_json_introspection_spec.rb +0 -141
  106. data/spec/ruact/testing/have_ruact_component_spec.rb +0 -170
  107. data/spec/ruact/testing/no_production_load_spec.rb +0 -41
  108. data/spec/ruact/validation_errors_spec.rb +0 -116
  109. data/spec/ruact/view_helper_spec.rb +0 -131
  110. data/spec/spec_helper.rb +0 -77
  111. data/spec/support/fixtures/pixel.png +0 -0
  112. data/spec/support/flight_wire_parser.rb +0 -21
  113. data/spec/support/flight_wire_parser_spec.rb +0 -93
  114. data/spec/support/matchers/flight_fixture_matcher.rb +0 -130
  115. data/spec/support/matchers/flight_fixture_matcher_spec.rb +0 -250
  116. data/spec/support/rails_stub.rb +0 -115
@@ -1,343 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "active_support/concern"
5
- # Re-run-5 (2026-05-15) — the `:current_user` inherited-helper clobber
6
- # test creates a `Class.new(ActionController::Base)`, which requires
7
- # `action_controller` to be loaded. Pre-Re-run-5 this test was the
8
- # first thing in the suite to demand-load `action_controller`,
9
- # which had the side effect of triggering `Rails::Application`'s
10
- # class definition AFTER spec_helper's `require "ruact"` ran — so
11
- # `Ruact::Railtie` never got loaded into the process and downstream
12
- # specs that depend on the Railtie's `to_prepare` hook would behave
13
- # inconsistently. Loading both explicitly here makes the order
14
- # deterministic.
15
- require "action_controller"
16
- require "ruact"
17
- require "ruact/controller"
18
-
19
- module Ruact
20
- RSpec.describe Controller do
21
- let(:test_class) do
22
- Class.new do
23
- include Ruact::Controller
24
-
25
- attr_reader :request
26
-
27
- def initialize(fake_request)
28
- @request = fake_request
29
- end
30
- end
31
- end
32
-
33
- # Minimal test double — `headers`, `format`, and `accepts` needed for the
34
- # methods under test. `format` must be a public struct member (not
35
- # Kernel#format) so that verify_partial_doubles can stub it. Story 10.0:
36
- # `#default_render` now keys off `request.accepts` (HTML acceptability), so
37
- # `accepts` is a settable member too.
38
- let(:fake_request) { Struct.new(:headers, :format, :accepts).new({}, nil, []) }
39
- let(:controller) { test_class.new(fake_request) }
40
-
41
- describe "#ruact_manifest" do
42
- # Opt OUT of spec_helper's suite-wide resolver stub here so this proves the
43
- # actual delegation: `#ruact_manifest` resolves through ManifestResolver
44
- # (prod returns the boot-loaded Ruact.manifest; dev fetches over HTTP).
45
- it "delegates manifest resolution to ManifestResolver.resolve" do
46
- test_manifest = ClientManifest.from_hash({})
47
- allow(ManifestResolver).to receive(:resolve).and_return(test_manifest)
48
-
49
- expect(controller.send(:ruact_manifest)).to be test_manifest
50
- expect(ManifestResolver).to have_received(:resolve)
51
- end
52
- end
53
-
54
- describe "#ruact_request?" do
55
- it "returns true when Accept: text/x-component" do
56
- fake_request.headers["Accept"] = "text/x-component"
57
- expect(controller.send(:ruact_request?)).to be true
58
- end
59
-
60
- it "returns true when Accept header includes text/x-component alongside other types" do
61
- fake_request.headers["Accept"] = "text/x-component, */*"
62
- expect(controller.send(:ruact_request?)).to be true
63
- end
64
-
65
- it "returns true when Ruact-Request: 1 header is set" do
66
- fake_request.headers["Ruact-Request"] = "1"
67
- expect(controller.send(:ruact_request?)).to be true
68
- end
69
-
70
- it "returns false when Accept: text/html" do
71
- fake_request.headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
72
- expect(controller.send(:ruact_request?)).to be false
73
- end
74
-
75
- it "returns false when no Accept header is set" do
76
- expect(controller.send(:ruact_request?)).to be false
77
- end
78
- end
79
-
80
- describe "#default_render" do
81
- # Story 10.0 — the activation predicate keys off HTML *acceptability*
82
- # (`request.accepts`), not `request.format.html?`. `request.format.html?`
83
- # is false for a `*/*` wildcard, which 500'd those requests pre-10.0.
84
- before do
85
- allow(controller).to receive(:ruact_template_exists?).and_return(true)
86
- allow(controller).to receive(:ruact_render)
87
- end
88
-
89
- it "calls ruact_render when the client accepts text/html and template exists (AC#1)" do
90
- fake_request.accepts = [Mime[:html]]
91
- controller.send(:default_render)
92
- expect(controller).to have_received(:ruact_render)
93
- end
94
-
95
- it "calls ruact_render for a */* wildcard Accept — Mime::ALL (Story 10.0 AC#1)" do
96
- fake_request.accepts = [Mime::ALL]
97
- controller.send(:default_render)
98
- expect(controller).to have_received(:ruact_render)
99
- end
100
-
101
- it "calls ruact_render when Accept is blank (defaults to HTML) (Story 10.0 AC#1)" do
102
- fake_request.accepts = []
103
- controller.send(:default_render)
104
- expect(controller).to have_received(:ruact_render)
105
- end
106
-
107
- it "calls ruact_render for RSC requests (text/x-component) even when HTML is unacceptable (AC#1)" do
108
- fake_request.accepts = [Mime[:json]]
109
- fake_request.headers["Ruact-Request"] = "1"
110
- controller.send(:default_render)
111
- expect(controller).to have_received(:ruact_render)
112
- end
113
-
114
- it "does NOT call ruact_render for a concrete non-HTML Accept that is not RSC (AC#4 — FR26)" do
115
- fake_request.accepts = [Mime[:json]]
116
- begin
117
- controller.send(:default_render)
118
- rescue StandardError
119
- nil
120
- end
121
- expect(controller).not_to have_received(:ruact_render)
122
- end
123
-
124
- it "does NOT call ruact_render when no template exists" do
125
- allow(controller).to receive(:ruact_template_exists?).and_return(false)
126
- fake_request.accepts = [Mime[:html]]
127
- begin
128
- controller.send(:default_render)
129
- rescue StandardError
130
- nil
131
- end
132
- expect(controller).not_to have_received(:ruact_render)
133
- end
134
- end
135
-
136
- describe "#redirect_to" do
137
- # Build a class hierarchy so `super` inside the override can call a base implementation.
138
- # url_for and render must be defined for verify_partial_doubles to allow stubbing them.
139
- let(:redirect_test_class) do
140
- base = Class.new do
141
- def redirect_to(*)
142
- :super_called
143
- end
144
-
145
- def url_for(options)
146
- options.is_a?(String) ? options : "/"
147
- end
148
-
149
- def render(**_opts); end
150
- end
151
- Class.new(base) do
152
- include Ruact::Controller
153
-
154
- attr_reader :request
155
-
156
- def initialize(req)
157
- super()
158
- @request = req
159
- end
160
- end
161
- end
162
-
163
- let(:ruact_ctrl) do
164
- redirect_test_class.new(Struct.new(:headers, :host).new({ "Accept" => "text/x-component" }, "localhost"))
165
- end
166
- let(:html_ctrl) do
167
- redirect_test_class.new(Struct.new(:headers, :host).new({}, "localhost"))
168
- end
169
-
170
- context "when RSC request with same-origin URL (AC #1, #5)" do
171
- before { allow(ruact_ctrl).to receive(:url_for).and_return("/posts/1") }
172
-
173
- it "calls render with a Flight redirect row (not a 302)" do
174
- allow(ruact_ctrl).to receive(:render)
175
- ruact_ctrl.send(:redirect_to, "/posts/1")
176
- expect(ruact_ctrl).to have_received(:render).with(
177
- plain: "0:{\"redirectUrl\":\"/posts/1\",\"redirectType\":\"push\"}\n",
178
- content_type: "text/x-component"
179
- )
180
- end
181
-
182
- it "redirect row matches the flight fixture (AC #5)" do
183
- rendered_plain = nil
184
- allow(ruact_ctrl).to receive(:render) { |opts| rendered_plain = opts[:plain] }
185
- ruact_ctrl.send(:redirect_to, "/posts/1")
186
- expect(rendered_plain).to match_flight_fixture("redirect_row")
187
- end
188
- end
189
-
190
- context "when RSC request with external URL (AC #3)" do
191
- before { allow(ruact_ctrl).to receive(:url_for).and_return("https://external.com/page") }
192
-
193
- it "does NOT emit a redirect row (falls back to super)" do
194
- allow(ruact_ctrl).to receive(:render)
195
- ruact_ctrl.send(:redirect_to, "https://external.com/page")
196
- expect(ruact_ctrl).not_to have_received(:render)
197
- end
198
- end
199
-
200
- context "when non-RSC request (AC #4)" do
201
- before { allow(html_ctrl).to receive(:url_for).and_return("/posts/1") }
202
-
203
- it "does NOT emit a redirect row (falls back to super)" do
204
- allow(html_ctrl).to receive(:render)
205
- html_ctrl.send(:redirect_to, "/posts/1")
206
- expect(html_ctrl).not_to have_received(:render)
207
- end
208
- end
209
- end
210
-
211
- describe "#ruact_html_shell" do
212
- # Story 14.2 — the entry `<script>` tags are emitted by the mixed-in
213
- # ViewHelper#ruact_vite_tags (which needs Rails.env / a live Vite probe).
214
- # Stub it to "" so we test the shell structure + the __FLIGHT_DATA script
215
- # (still emitted by ruact_js_assets) in isolation.
216
- before { allow(controller).to receive(:ruact_vite_tags).and_return("") }
217
-
218
- let(:payload) { "0:[\"$\",\"div\",null,{}]\n" }
219
-
220
- it "returns a string containing window.__FLIGHT_DATA" do
221
- html = controller.send(:ruact_html_shell, payload)
222
- expect(html).to include("__FLIGHT_DATA")
223
- end
224
-
225
- it "wraps the payload in an IIFE push" do
226
- html = controller.send(:ruact_html_shell, payload)
227
- expect(html).to include("d.push(")
228
- end
229
-
230
- it "contains a root div#root element" do
231
- html = controller.send(:ruact_html_shell, payload)
232
- expect(html).to include('<div id="root">')
233
- end
234
-
235
- it "escapes </script> in the payload to prevent XSS breakout" do
236
- dangerous_payload = "0:\"</script><script>alert(1)</script>\"\n"
237
- html = controller.send(:ruact_html_shell, dangerous_payload)
238
- # The HTML must contain exactly ONE </script> — the real closing tag of the script block.
239
- # If the payload's </script> leaked through, there would be more than one.
240
- occurrences = html.scan("</script>")
241
- count = occurrences.length
242
- expect(count).to eq(1), "Expected 1 </script> (closing tag), found #{count}"
243
- end
244
-
245
- # Story 8.3 review R7 — the shell must surface the host's CSRF token
246
- # as a `<meta name="csrf-token">` tag so the JS runtime can forward
247
- # it as `X-CSRF-Token` on every `_makeRef` call. Without this,
248
- # standalone server actions (Story 8.3 AC5) — for which the gem
249
- # enforces CSRF itself via `protect_from_forgery with: :exception,
250
- # if: :dispatching_standalone?` — can never authenticate, because
251
- # the document has no token for the runtime to read.
252
- context "Story 8.3 — CSRF meta tag injection", :story_8_3 do
253
- # The bare `test_class` above is a minimal `include Ruact::Controller`
254
- # consumer with no `form_authenticity_token` surface (no Rails
255
- # request-forgery-protection module mixed in). Define a real method
256
- # on the class so `respond_to?(:form_authenticity_token, true)` is
257
- # true AND `verify_partial_doubles` lets us stub the return value.
258
- let(:csrf_test_class) do
259
- Class.new(test_class) do
260
- def form_authenticity_token
261
- "stub-default-token"
262
- end
263
- end
264
- end
265
- let(:csrf_controller) { csrf_test_class.new(fake_request) }
266
-
267
- # Story 14.2 — csrf_controller is a separate instance from `controller`,
268
- # so the outer `before` stub does not reach it. Stub its entry-tag
269
- # emission too (the real path would hit Rails.root in a non-booted env),
270
- # keeping these CSRF tests order-independent.
271
- before { allow(csrf_controller).to receive(:ruact_vite_tags).and_return("") }
272
-
273
- it "embeds <meta name=\"csrf-token\" content=\"...\"> when the host exposes form_authenticity_token" do
274
- allow(csrf_controller).to receive(:form_authenticity_token).and_return("test-csrf-token-value")
275
- html = csrf_controller.send(:ruact_html_shell, payload)
276
- expect(html).to include('<meta name="csrf-token" content="test-csrf-token-value" />')
277
- end
278
-
279
- it "HTML-escapes the token value (defense against accidental quote injection)" do
280
- allow(csrf_controller).to receive(:form_authenticity_token).and_return('"quoted" & <evil>')
281
- html = csrf_controller.send(:ruact_html_shell, payload)
282
- expect(html).to include("&quot;quoted&quot; &amp; &lt;evil&gt;")
283
- expect(html).not_to include('"quoted" & <evil>')
284
- end
285
-
286
- it "omits the meta tag (renders empty string) when form_authenticity_token is not available " \
287
- "(e.g., a non-Rails spec context)" do
288
- # `controller` is the bare test_class without `form_authenticity_token`.
289
- html = controller.send(:ruact_html_shell, payload)
290
- expect(html).not_to include('name="csrf-token"')
291
- end
292
-
293
- it "omits the meta tag when form_authenticity_token returns nil/empty" do
294
- allow(csrf_controller).to receive(:form_authenticity_token).and_return("")
295
- html = csrf_controller.send(:ruact_html_shell, payload)
296
- expect(html).not_to include('name="csrf-token"')
297
- end
298
-
299
- it "silently omits the meta tag if form_authenticity_token raises " \
300
- "(e.g., session middleware missing in a stripped-down test env)" do
301
- allow(csrf_controller).to receive(:form_authenticity_token).and_raise(StandardError, "no session")
302
- expect { csrf_controller.send(:ruact_html_shell, payload) }.not_to raise_error
303
- html = csrf_controller.send(:ruact_html_shell, payload)
304
- expect(html).not_to include('name="csrf-token"')
305
- end
306
- end
307
- end
308
-
309
- # Story 14.2 (FR104) — the controller's HTML shell delegates its JS asset
310
- # markup (entry tags + __FLIGHT_DATA) to the single
311
- # Ruact::ViewHelper#ruact_js_assets implementation. No duplicated tag logic;
312
- # controller output == a view helper's output (parity guards against drift).
313
- describe "ruact_js_assets delegation + parity", :story_14_2 do
314
- let(:payload) { "0:[\"$\",\"div\",null,{}]\n" }
315
-
316
- it "delegates the JS block to ruact_js_assets (single implementation)" do
317
- allow(controller).to receive(:ruact_js_assets).with(payload).and_return("<!--RUACT-JS-->".html_safe)
318
- html = controller.send(:ruact_html_shell, payload)
319
- expect(html).to include("<!--RUACT-JS-->")
320
- expect(controller).to have_received(:ruact_js_assets).with(payload)
321
- end
322
-
323
- it "emits markup byte-identical to a standalone view helper (no drift)" do
324
- allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production"))
325
- manifest_entry = { "file" => "bootstrap-xyz789.js" }
326
- allow(controller).to receive(:vite_manifest_entry).and_return(manifest_entry)
327
-
328
- view = Object.new
329
- view.extend(Ruact::ViewHelper)
330
- allow(view).to receive(:vite_manifest_entry).and_return(manifest_entry)
331
-
332
- expect(controller.send(:ruact_js_assets, payload)).to eq(view.ruact_js_assets(payload))
333
- end
334
-
335
- it "exposes ruact_js_assets as PRIVATE on the controller (never a routable action)", :aggregate_failures do
336
- expect(controller.class.public_method_defined?(:ruact_js_assets)).to be false
337
- expect(controller.class.private_method_defined?(:ruact_js_assets)).to be true
338
- # __ruact_component__ is likewise demoted so it is not exposed as an action.
339
- expect(controller.class.public_method_defined?(:__ruact_component__)).to be false
340
- end
341
- end
342
- end
343
- end