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.
- checksums.yaml +4 -4
- data/.codecov.yml +31 -0
- data/.github/workflows/ci.yml +160 -94
- data/.github/workflows/server-functions-bench.yml +54 -0
- data/.rubocop.yml +19 -1
- data/.rubocop_todo.yml +175 -0
- data/CHANGELOG.md +88 -5
- data/README.md +2 -0
- data/RELEASING.md +9 -3
- data/bench/server_functions_dispatch_bench.rb +309 -0
- data/bench/server_functions_dispatch_bench.results.md +121 -0
- data/docs/internal/README.md +9 -0
- data/docs/internal/decisions/server-functions-api.md +1779 -0
- data/lib/generators/ruact/install/install_generator.rb +43 -0
- data/lib/generators/ruact/install/templates/application.jsx.tt +1 -1
- data/lib/generators/ruact/install/templates/initializer.rb.tt +1 -1
- data/lib/ruact/client_manifest.rb +125 -12
- data/lib/ruact/configuration.rb +264 -23
- data/lib/ruact/controller.rb +459 -32
- data/lib/ruact/doctor.rb +34 -2
- data/lib/ruact/erb_preprocessor.rb +6 -6
- data/lib/ruact/errors.rb +100 -0
- data/lib/ruact/flight/serializer.rb +2 -2
- data/lib/ruact/html_converter.rb +131 -31
- data/lib/ruact/query.rb +107 -0
- data/lib/ruact/railtie.rb +220 -3
- data/lib/ruact/render_context.rb +30 -0
- data/lib/ruact/render_pipeline.rb +201 -59
- data/lib/ruact/routing.rb +81 -0
- data/lib/ruact/serializable.rb +11 -11
- data/lib/ruact/server.rb +341 -0
- data/lib/ruact/server_action.rb +131 -0
- data/lib/ruact/server_functions/backtrace_cleaner.rb +32 -0
- data/lib/ruact/server_functions/bucket_two_payload.rb +109 -0
- data/lib/ruact/server_functions/codegen.rb +344 -0
- data/lib/ruact/server_functions/codegen_v2.rb +212 -0
- data/lib/ruact/server_functions/endpoint_controller.rb +237 -0
- data/lib/ruact/server_functions/error_payload.rb +93 -0
- data/lib/ruact/server_functions/error_rendering.rb +190 -0
- data/lib/ruact/server_functions/error_suggestion.rb +38 -0
- data/lib/ruact/server_functions/name_bridge.rb +118 -0
- data/lib/ruact/server_functions/query_context.rb +62 -0
- data/lib/ruact/server_functions/query_dispatch.rb +313 -0
- data/lib/ruact/server_functions/query_source.rb +150 -0
- data/lib/ruact/server_functions/registry.rb +148 -0
- data/lib/ruact/server_functions/registry_entry.rb +26 -0
- data/lib/ruact/server_functions/route_source.rb +201 -0
- data/lib/ruact/server_functions/snapshot.rb +195 -0
- data/lib/ruact/server_functions/snapshot_writer.rb +65 -0
- data/lib/ruact/server_functions/standalone_context.rb +103 -0
- data/lib/ruact/server_functions/standalone_dispatcher.rb +178 -0
- data/lib/ruact/server_functions.rb +111 -0
- data/lib/ruact/version.rb +1 -1
- data/lib/ruact/view_helper.rb +17 -9
- data/lib/ruact.rb +85 -6
- data/lib/rubocop/cop/ruact/no_shared_state.rb +1 -1
- data/lib/tasks/benchmark.rake +15 -11
- data/lib/tasks/ruact.rake +81 -0
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +1 -1
- data/spec/fixtures/flight/README.md +55 -7
- data/spec/fixtures/flight/bigint.txt +1 -0
- data/spec/fixtures/flight/infinity.txt +1 -0
- data/spec/fixtures/flight/nan.txt +1 -0
- data/spec/fixtures/flight/negative_infinity.txt +1 -0
- data/spec/fixtures/flight/undefined.txt +1 -0
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +3 -0
- data/spec/ruact/client_manifest_spec.rb +108 -0
- data/spec/ruact/configuration_spec.rb +501 -0
- data/spec/ruact/controller_request_spec.rb +204 -0
- data/spec/ruact/controller_spec.rb +427 -39
- data/spec/ruact/doctor_spec.rb +118 -0
- data/spec/ruact/erb_preprocessor_hook_spec.rb +3 -3
- data/spec/ruact/erb_preprocessor_spec.rb +7 -7
- data/spec/ruact/errors_spec.rb +95 -0
- data/spec/ruact/flight/renderer_spec.rb +14 -3
- data/spec/ruact/flight/serializer_spec.rb +129 -88
- data/spec/ruact/html_converter_spec.rb +183 -5
- data/spec/ruact/install_generator_spec.rb +93 -0
- data/spec/ruact/query_request_spec.rb +598 -0
- data/spec/ruact/query_spec.rb +105 -0
- data/spec/ruact/railtie_spec.rb +2 -3
- data/spec/ruact/render_context_spec.rb +58 -0
- data/spec/ruact/render_pipeline_concurrency_spec.rb +78 -0
- data/spec/ruact/render_pipeline_spec.rb +784 -330
- data/spec/ruact/serializable_spec.rb +8 -8
- data/spec/ruact/server_bucket_request_spec.rb +352 -0
- data/spec/ruact/server_function_name_spec.rb +53 -0
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +63 -0
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +200 -0
- data/spec/ruact/server_functions/codegen_spec.rb +508 -0
- data/spec/ruact/server_functions/csrf_request_spec.rb +380 -0
- data/spec/ruact/server_functions/dispatch_request_spec.rb +819 -0
- data/spec/ruact/server_functions/error_payload_spec.rb +222 -0
- data/spec/ruact/server_functions/error_suggestion_spec.rb +79 -0
- data/spec/ruact/server_functions/name_bridge_spec.rb +212 -0
- data/spec/ruact/server_functions/query_context_spec.rb +72 -0
- data/spec/ruact/server_functions/query_source_spec.rb +142 -0
- data/spec/ruact/server_functions/railtie_integration_spec.rb +412 -0
- data/spec/ruact/server_functions/rake_spec.rb +86 -0
- data/spec/ruact/server_functions/registry_spec.rb +199 -0
- data/spec/ruact/server_functions/route_source_spec.rb +202 -0
- data/spec/ruact/server_functions/snapshot_spec.rb +256 -0
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +71 -0
- data/spec/ruact/server_functions/standalone_action_spec.rb +224 -0
- data/spec/ruact/server_functions/standalone_context_spec.rb +142 -0
- data/spec/ruact/server_functions/standalone_dispatcher_spec.rb +273 -0
- data/spec/ruact/server_rescue_request_spec.rb +416 -0
- data/spec/ruact/server_spec.rb +180 -0
- data/spec/ruact/server_upload_request_spec.rb +311 -0
- data/spec/ruact/view_helper_spec.rb +23 -17
- data/spec/spec_helper.rb +52 -1
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +135 -0
- data/spec/support/flight_wire_parser_spec.rb +93 -0
- data/spec/support/matchers/flight_fixture_matcher.rb +356 -0
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +250 -0
- data/spec/support/rails_stub.rb +75 -5
- data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +173 -0
- data/vendor/javascript/ruact-server-functions-runtime/index.js +614 -0
- data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +827 -0
- data/vendor/javascript/ruact-server-functions-runtime/package.json +29 -0
- data/vendor/javascript/ruact-server-functions-runtime/usequery.test.mjs +181 -0
- data/vendor/javascript/vite-plugin-ruact/index.js +164 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +1429 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +15 -0
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs +804 -0
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs +961 -0
- data/vendor/javascript/vite-plugin-ruact/vitest.config.mjs +21 -0
- metadata +91 -5
- data/lib/ruact/component_registry.rb +0 -31
- 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
|