ruact 0.0.2 → 0.0.4

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 (131) hide show
  1. checksums.yaml +4 -4
  2. data/.codecov.yml +31 -0
  3. data/.github/workflows/ci.yml +160 -94
  4. data/.github/workflows/server-functions-bench.yml +54 -0
  5. data/.rubocop.yml +19 -1
  6. data/.rubocop_todo.yml +175 -0
  7. data/CHANGELOG.md +88 -5
  8. data/README.md +2 -0
  9. data/RELEASING.md +9 -3
  10. data/bench/server_functions_dispatch_bench.rb +309 -0
  11. data/bench/server_functions_dispatch_bench.results.md +121 -0
  12. data/docs/internal/README.md +9 -0
  13. data/docs/internal/decisions/server-functions-api.md +1779 -0
  14. data/lib/generators/ruact/install/install_generator.rb +43 -0
  15. data/lib/generators/ruact/install/templates/application.jsx.tt +1 -1
  16. data/lib/generators/ruact/install/templates/initializer.rb.tt +1 -1
  17. data/lib/ruact/client_manifest.rb +125 -12
  18. data/lib/ruact/configuration.rb +264 -23
  19. data/lib/ruact/controller.rb +459 -32
  20. data/lib/ruact/doctor.rb +34 -2
  21. data/lib/ruact/erb_preprocessor.rb +6 -6
  22. data/lib/ruact/errors.rb +100 -0
  23. data/lib/ruact/flight/serializer.rb +2 -2
  24. data/lib/ruact/html_converter.rb +131 -31
  25. data/lib/ruact/query.rb +107 -0
  26. data/lib/ruact/railtie.rb +220 -3
  27. data/lib/ruact/render_context.rb +30 -0
  28. data/lib/ruact/render_pipeline.rb +201 -59
  29. data/lib/ruact/routing.rb +81 -0
  30. data/lib/ruact/serializable.rb +11 -11
  31. data/lib/ruact/server.rb +341 -0
  32. data/lib/ruact/server_action.rb +131 -0
  33. data/lib/ruact/server_functions/backtrace_cleaner.rb +32 -0
  34. data/lib/ruact/server_functions/bucket_two_payload.rb +109 -0
  35. data/lib/ruact/server_functions/codegen.rb +344 -0
  36. data/lib/ruact/server_functions/codegen_v2.rb +212 -0
  37. data/lib/ruact/server_functions/endpoint_controller.rb +237 -0
  38. data/lib/ruact/server_functions/error_payload.rb +93 -0
  39. data/lib/ruact/server_functions/error_rendering.rb +190 -0
  40. data/lib/ruact/server_functions/error_suggestion.rb +38 -0
  41. data/lib/ruact/server_functions/name_bridge.rb +118 -0
  42. data/lib/ruact/server_functions/query_context.rb +62 -0
  43. data/lib/ruact/server_functions/query_dispatch.rb +313 -0
  44. data/lib/ruact/server_functions/query_source.rb +150 -0
  45. data/lib/ruact/server_functions/registry.rb +148 -0
  46. data/lib/ruact/server_functions/registry_entry.rb +26 -0
  47. data/lib/ruact/server_functions/route_source.rb +201 -0
  48. data/lib/ruact/server_functions/snapshot.rb +195 -0
  49. data/lib/ruact/server_functions/snapshot_writer.rb +65 -0
  50. data/lib/ruact/server_functions/standalone_context.rb +103 -0
  51. data/lib/ruact/server_functions/standalone_dispatcher.rb +178 -0
  52. data/lib/ruact/server_functions.rb +111 -0
  53. data/lib/ruact/version.rb +1 -1
  54. data/lib/ruact/view_helper.rb +17 -9
  55. data/lib/ruact.rb +85 -6
  56. data/lib/rubocop/cop/ruact/no_shared_state.rb +1 -1
  57. data/lib/tasks/benchmark.rake +15 -11
  58. data/lib/tasks/ruact.rake +81 -0
  59. data/spec/benchmarks/render_pipeline_benchmark_spec.rb +1 -1
  60. data/spec/fixtures/flight/README.md +55 -7
  61. data/spec/fixtures/flight/bigint.txt +1 -0
  62. data/spec/fixtures/flight/infinity.txt +1 -0
  63. data/spec/fixtures/flight/nan.txt +1 -0
  64. data/spec/fixtures/flight/negative_infinity.txt +1 -0
  65. data/spec/fixtures/flight/undefined.txt +1 -0
  66. data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +3 -0
  67. data/spec/ruact/client_manifest_spec.rb +108 -0
  68. data/spec/ruact/configuration_spec.rb +501 -0
  69. data/spec/ruact/controller_request_spec.rb +204 -0
  70. data/spec/ruact/controller_spec.rb +427 -39
  71. data/spec/ruact/doctor_spec.rb +118 -0
  72. data/spec/ruact/erb_preprocessor_hook_spec.rb +3 -3
  73. data/spec/ruact/erb_preprocessor_spec.rb +7 -7
  74. data/spec/ruact/errors_spec.rb +95 -0
  75. data/spec/ruact/flight/renderer_spec.rb +14 -3
  76. data/spec/ruact/flight/serializer_spec.rb +129 -88
  77. data/spec/ruact/html_converter_spec.rb +183 -5
  78. data/spec/ruact/install_generator_spec.rb +93 -0
  79. data/spec/ruact/query_request_spec.rb +598 -0
  80. data/spec/ruact/query_spec.rb +105 -0
  81. data/spec/ruact/railtie_spec.rb +2 -3
  82. data/spec/ruact/render_context_spec.rb +58 -0
  83. data/spec/ruact/render_pipeline_concurrency_spec.rb +78 -0
  84. data/spec/ruact/render_pipeline_spec.rb +784 -330
  85. data/spec/ruact/serializable_spec.rb +8 -8
  86. data/spec/ruact/server_bucket_request_spec.rb +352 -0
  87. data/spec/ruact/server_function_name_spec.rb +53 -0
  88. data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +63 -0
  89. data/spec/ruact/server_functions/bucket_two_payload_spec.rb +200 -0
  90. data/spec/ruact/server_functions/codegen_spec.rb +508 -0
  91. data/spec/ruact/server_functions/csrf_request_spec.rb +380 -0
  92. data/spec/ruact/server_functions/dispatch_request_spec.rb +819 -0
  93. data/spec/ruact/server_functions/error_payload_spec.rb +222 -0
  94. data/spec/ruact/server_functions/error_suggestion_spec.rb +79 -0
  95. data/spec/ruact/server_functions/name_bridge_spec.rb +212 -0
  96. data/spec/ruact/server_functions/query_context_spec.rb +72 -0
  97. data/spec/ruact/server_functions/query_source_spec.rb +142 -0
  98. data/spec/ruact/server_functions/railtie_integration_spec.rb +412 -0
  99. data/spec/ruact/server_functions/rake_spec.rb +86 -0
  100. data/spec/ruact/server_functions/registry_spec.rb +199 -0
  101. data/spec/ruact/server_functions/route_source_spec.rb +202 -0
  102. data/spec/ruact/server_functions/snapshot_spec.rb +256 -0
  103. data/spec/ruact/server_functions/snapshot_writer_spec.rb +71 -0
  104. data/spec/ruact/server_functions/standalone_action_spec.rb +224 -0
  105. data/spec/ruact/server_functions/standalone_context_spec.rb +142 -0
  106. data/spec/ruact/server_functions/standalone_dispatcher_spec.rb +273 -0
  107. data/spec/ruact/server_rescue_request_spec.rb +416 -0
  108. data/spec/ruact/server_spec.rb +180 -0
  109. data/spec/ruact/server_upload_request_spec.rb +311 -0
  110. data/spec/ruact/view_helper_spec.rb +23 -17
  111. data/spec/spec_helper.rb +52 -1
  112. data/spec/support/fixtures/pixel.png +0 -0
  113. data/spec/support/flight_wire_parser.rb +135 -0
  114. data/spec/support/flight_wire_parser_spec.rb +93 -0
  115. data/spec/support/matchers/flight_fixture_matcher.rb +356 -0
  116. data/spec/support/matchers/flight_fixture_matcher_spec.rb +250 -0
  117. data/spec/support/rails_stub.rb +75 -5
  118. data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +173 -0
  119. data/vendor/javascript/ruact-server-functions-runtime/index.js +614 -0
  120. data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +827 -0
  121. data/vendor/javascript/ruact-server-functions-runtime/package.json +29 -0
  122. data/vendor/javascript/ruact-server-functions-runtime/usequery.test.mjs +181 -0
  123. data/vendor/javascript/vite-plugin-ruact/index.js +164 -0
  124. data/vendor/javascript/vite-plugin-ruact/package-lock.json +1429 -0
  125. data/vendor/javascript/vite-plugin-ruact/package.json +15 -0
  126. data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs +804 -0
  127. data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs +961 -0
  128. data/vendor/javascript/vite-plugin-ruact/vitest.config.mjs +21 -0
  129. metadata +91 -5
  130. data/lib/ruact/component_registry.rb +0 -31
  131. data/lib/tasks/rsc.rake +0 -9
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Story 7.9 — Bug 7.8-B regression spec.
4
+ #
5
+ # Exercises Ruact::Controller end-to-end through Rails 8's full controller
6
+ # lifecycle (ActionController::Base#dispatch → ActionView::Base allocation →
7
+ # template render → __ruact_component__ helper). The bug closed by this spec
8
+ # was invisible to unit-only controller_spec.rb because that file stubs
9
+ # render_to_string and never instantiates ActionView::Base.
10
+ #
11
+ # Rails request-cycle subsystems are loaded HERE (not in spec_helper.rb) so
12
+ # the rest of the suite continues to use spec/support/rails_stub.rb without
13
+ # paying the action_controller / action_view boot cost.
14
+ require "action_controller/railtie"
15
+ require "action_view/railtie"
16
+
17
+ require "spec_helper"
18
+
19
+ require "rack/test"
20
+ require "tmpdir"
21
+ require "fileutils"
22
+
23
+ # Ruact::Controller is normally loaded by the Railtie's `ruact.load_controller`
24
+ # initializer at app boot. This spec instantiates a Rails::Application but does
25
+ # not depend on initializer ordering, so we load the concern explicitly.
26
+ require "ruact/controller"
27
+
28
+ # Test-support helpers and demo controller — top-level so the Rails routes
29
+ # resolver finds them. Defined before the describe block so the controller
30
+ # class exists when routes are appended.
31
+ module ControllerRequestSpecSupport
32
+ class << self
33
+ attr_reader :manifest_path
34
+
35
+ def app_class
36
+ @app_class ||= build_app_class
37
+ end
38
+
39
+ def boot!
40
+ return if @booted
41
+
42
+ @tmpdir = Dir.mktmpdir("ruact-story-7-9")
43
+ @manifest_path = File.join(@tmpdir, "react-client-manifest.json")
44
+ File.write(@manifest_path, JSON.dump(
45
+ "DemoButton" => {
46
+ "id" => "/DemoButton.jsx",
47
+ "name" => "DemoButton",
48
+ "chunks" => ["/DemoButton.jsx"]
49
+ }
50
+ ))
51
+
52
+ # Story 7.3: Ruact.config is frozen after the first configure block. The
53
+ # spec_helper before hook resets it between examples; the describe's
54
+ # per-example `before` re-primes the manifest_path. Here we just put the
55
+ # initial state in place and cache the manifest at module level so it
56
+ # survives subsequent config resets.
57
+ Ruact.instance_variable_set(:@config, nil)
58
+ Ruact.instance_variable_set(:@configured_at_least_once, false)
59
+ Ruact.configure do |c|
60
+ c.manifest_path = @manifest_path
61
+ end
62
+ Ruact.manifest # prime the cache
63
+
64
+ app_class.instance.initialize!
65
+ @booted = true
66
+ end
67
+
68
+ private
69
+
70
+ def build_app_class
71
+ Class.new(Rails::Application) do
72
+ config.eager_load = false
73
+ config.consider_all_requests_local = true
74
+ config.action_controller.perform_caching = false
75
+ config.action_dispatch.show_exceptions = :none
76
+ config.logger = Logger.new(IO::NULL)
77
+ config.active_support.deprecation = :silence
78
+ config.secret_key_base = "x" * 64
79
+ config.hosts.clear if config.respond_to?(:hosts)
80
+
81
+ routes.append do
82
+ get "/demo/show", to: "controller_request_spec_support/demo#show"
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ # Demo controller for the spec. Uses an inline `append_view_path` instead of
89
+ # relying on Rails.root/app/views, so Controller#ruact_render is invoked
90
+ # directly from #show rather than via default_render (which short-circuits
91
+ # when the conventional template path does not exist on disk).
92
+ class DemoController < ActionController::Base
93
+ include Ruact::Controller
94
+
95
+ append_view_path File.expand_path("../fixtures/story_7_9_views", __dir__)
96
+
97
+ def show
98
+ ruact_render
99
+ end
100
+ end
101
+ end
102
+
103
+ # Reset Rails.application so this spec can boot its own minimal app even if a
104
+ # prior spec ran a different Rails::Application subclass (the constant is
105
+ # memoized in Rails::Application; we own the reset here).
106
+ Rails.application = nil if Rails.respond_to?(:application) && !Rails.application.is_a?(Rails::Application)
107
+
108
+ module Ruact # rubocop:disable Style/OneClassPerFile
109
+ RSpec.describe "Story 7.9: Bug 7.8-B — render_to_string view-context ivar handoff", :story_7_9 do
110
+ include Rack::Test::Methods
111
+
112
+ let(:app_class) { ControllerRequestSpecSupport.app_class }
113
+ let(:app) { app_class.instance }
114
+
115
+ # Boot lives inside a per-example `before` hook (not `before(:all)`) so
116
+ # RSpec's per-test lifecycle for rspec-mocks is active when Rails
117
+ # initializers fire. Boot is memoized so the cost is paid once across all
118
+ # examples.
119
+ before do
120
+ # railtie_spec.rb assigns Rails.logger to an instance_double via the
121
+ # writer (not via rspec-mocks); the assignment persists across examples
122
+ # and the request-cycle code path calls Rails.logger.* — replace with a
123
+ # real logger that survives the example lifecycle.
124
+ Rails.logger = Logger.new(IO::NULL)
125
+ ControllerRequestSpecSupport.boot!
126
+ # Re-prime Ruact.config after spec_helper's per-example reset wiped it.
127
+ Ruact.configure do |c|
128
+ c.manifest_path = ControllerRequestSpecSupport.manifest_path
129
+ end
130
+ end
131
+
132
+ describe "happy path: PascalCase tag renders successfully" do
133
+ it "GET /demo/show returns 200 with the Flight payload (was 500 pre-7.9)" do
134
+ get "/demo/show"
135
+ expect(last_response.status).to(eq(200),
136
+ "expected 200, got #{last_response.status} body=#{last_response.body[0, 400]}")
137
+ expect(last_response.body).to include("DemoButton")
138
+ end
139
+
140
+ it "RSC request returns text/x-component with the registered component" do
141
+ get "/demo/show", {}, { "HTTP_ACCEPT" => "text/x-component" }
142
+ expect(last_response.status).to eq(200)
143
+ expect(last_response.headers["Content-Type"]).to include("text/x-component")
144
+ expect(last_response.body).to include("DemoButton")
145
+ end
146
+ end
147
+
148
+ describe "regression guard: render context reaches ViewHelper" do
149
+ it "ViewHelper#__ruact_component__ does NOT raise the outside-flow error" do
150
+ # If the handoff regresses, the request returns 500 with this exact
151
+ # message in the body or raises the exception in the request chain.
152
+ get "/demo/show"
153
+ expect(last_response.body).not_to include("__ruact_component__ called outside a ruact_render flow")
154
+ end
155
+
156
+ it "Flight payload contains the registered component import row" do
157
+ # End-to-end probe: a successful render emits a Flight I-row (import)
158
+ # for every registered client component. The presence of the
159
+ # DemoButton's module path proves the handoff ran end-to-end rather
160
+ # than the request happening to 200 for some unrelated reason.
161
+ get "/demo/show", {}, { "HTTP_ACCEPT" => "text/x-component" }
162
+ expect(last_response.status).to eq(200)
163
+ expect(last_response.body).to match(%r{\h+:I\[.*/DemoButton\.jsx}),
164
+ "expected a Flight I-row referencing /DemoButton.jsx; " \
165
+ "got: #{last_response.body[0, 400]}"
166
+ end
167
+
168
+ it "render context is cleared after the render returns" do
169
+ # AC-adjacent regression: the controller's ensure block must restore
170
+ # @ruact_render_context so a subsequent error/rescue render in the
171
+ # same request cannot see stale context. Re-issuing the same request
172
+ # on the same Rack stack exercises the per-request lifecycle.
173
+ get "/demo/show"
174
+ get "/demo/show"
175
+ expect(last_response.status).to eq(200)
176
+ end
177
+
178
+ it "ensure block removes the ivar entirely when it was not defined before" do
179
+ # Direct unit-level guard: when ruact_render starts on a controller
180
+ # instance with no prior @ruact_render_context, the ensure block
181
+ # must put the controller back into the instance_variable_defined?
182
+ # = false state — not into the "ivar is defined as nil" state,
183
+ # which would leak a phantom `{"ruact_render_context" => nil}`
184
+ # entry into view_assigns on any subsequent error render.
185
+ controller = ControllerRequestSpecSupport::DemoController.new
186
+ controller.instance_variable_set(:@_request, nil)
187
+ # Invoke just the ivar lifecycle by stubbing the heavy parts.
188
+ allow(controller).to receive_messages(
189
+ render_to_string: "<div></div>",
190
+ ruact_request?: false,
191
+ ruact_manifest: Ruact.manifest,
192
+ controller_path: "controller_request_spec_support/demo",
193
+ logger: Logger.new(IO::NULL),
194
+ action_name: "show"
195
+ )
196
+ allow(controller).to receive(:render)
197
+
198
+ expect(controller.instance_variable_defined?(:@ruact_render_context)).to be(false)
199
+ controller.send(:ruact_render)
200
+ expect(controller.instance_variable_defined?(:@ruact_render_context)).to be(false)
201
+ end
202
+ end
203
+ end
204
+ end