react_on_rails 16.2.0.beta.4 → 16.2.0.beta.10
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 +36 -8
- data/CONTRIBUTING.md +1 -1
- data/Gemfile.development_dependencies +0 -1
- data/Gemfile.lock +1 -9
- data/bin/ci-rerun-failures +39 -16
- data/bin/ci-run-failed-specs +1 -1
- data/bin/ci-switch-config +8 -2
- data/bin/lefthook/ruby-autofix +2 -1
- data/knip.ts +35 -9
- data/lib/generators/react_on_rails/base_generator.rb +3 -118
- data/lib/generators/react_on_rails/install_generator.rb +5 -180
- data/lib/generators/react_on_rails/js_dependency_manager.rb +332 -0
- data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +32 -52
- data/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml +14 -3
- data/lib/react_on_rails/dev/server_manager.rb +11 -4
- data/lib/react_on_rails/doctor.rb +245 -0
- data/lib/react_on_rails/engine.rb +2 -5
- data/lib/react_on_rails/helper.rb +9 -0
- data/lib/react_on_rails/version.rb +1 -1
- data/react_on_rails_pro/CHANGELOG.md +7 -0
- data/react_on_rails_pro/CONTRIBUTING.md +2 -13
- data/react_on_rails_pro/Gemfile.lock +21 -3
- data/react_on_rails_pro/docs/code-splitting-loadable-components.md +1 -1
- data/react_on_rails_pro/docs/contributors-info/releasing.md +2 -2
- data/react_on_rails_pro/docs/installation.md +106 -104
- data/react_on_rails_pro/docs/node-renderer/basics.md +3 -3
- data/react_on_rails_pro/docs/node-renderer/error-reporting-and-tracing.md +8 -8
- data/react_on_rails_pro/docs/node-renderer/js-configuration.md +1 -1
- data/react_on_rails_pro/docs/updating.md +209 -15
- data/react_on_rails_pro/lib/react_on_rails_pro/concerns/stream.rb +58 -4
- data/react_on_rails_pro/lib/react_on_rails_pro/configuration.rb +17 -3
- data/react_on_rails_pro/lib/react_on_rails_pro/license_public_key.rb +9 -9
- data/react_on_rails_pro/lib/react_on_rails_pro/request.rb +41 -25
- data/react_on_rails_pro/lib/react_on_rails_pro/stream_request.rb +27 -7
- data/react_on_rails_pro/lib/react_on_rails_pro/utils.rb +3 -3
- data/react_on_rails_pro/lib/react_on_rails_pro/version.rb +1 -1
- data/react_on_rails_pro/package-scripts.yml +1 -1
- data/react_on_rails_pro/package.json +5 -8
- data/react_on_rails_pro/packages/node-renderer/src/integrations/api.ts +1 -1
- data/react_on_rails_pro/rakelib/public_key_management.rake +6 -5
- data/react_on_rails_pro/react_on_rails_pro.gemspec +1 -0
- data/react_on_rails_pro/spec/dummy/Gemfile.lock +20 -3
- data/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb +3 -3
- data/react_on_rails_pro/spec/dummy/bin/dev +4 -8
- data/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook +19 -0
- data/react_on_rails_pro/spec/dummy/client/node-renderer.js +3 -3
- data/react_on_rails_pro/spec/dummy/config/environments/production.rb +1 -1
- data/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb +28 -12
- data/react_on_rails_pro/spec/dummy/config/shakapacker.yml +5 -0
- data/react_on_rails_pro/spec/dummy/config.ru +1 -1
- data/react_on_rails_pro/spec/dummy/package.json +2 -2
- data/react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb +40 -11
- data/react_on_rails_pro/spec/dummy/spec/rails_helper.rb +1 -1
- data/react_on_rails_pro/spec/dummy/spec/requests/renderer_console_logging_spec.rb +5 -5
- data/react_on_rails_pro/spec/dummy/spec/system/integration_spec.rb +20 -14
- data/react_on_rails_pro/spec/dummy/spec/system/renderer_integration_spec.rb +3 -3
- data/react_on_rails_pro/spec/dummy/yarn.lock +4 -4
- data/react_on_rails_pro/spec/execjs-compatible-dummy/config/environments/production.rb +1 -1
- data/react_on_rails_pro/spec/execjs-compatible-dummy/config/initializers/react_on_rails.rb +16 -43
- data/react_on_rails_pro/spec/react_on_rails_pro/assets_precompile_spec.rb +15 -18
- data/react_on_rails_pro/spec/react_on_rails_pro/cache_spec.rb +1 -1
- data/react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb +5 -3
- data/react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb +27 -12
- data/react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb +0 -27
- data/react_on_rails_pro/spec/react_on_rails_pro/spec_helper.rb +1 -1
- data/react_on_rails_pro/spec/react_on_rails_pro/stream_decorator_spec.rb +89 -0
- data/react_on_rails_pro/spec/react_on_rails_pro/stream_spec.rb +144 -0
- data/react_on_rails_pro/spec/react_on_rails_pro/support/caching.rb +1 -1
- data/react_on_rails_pro/spec/react_on_rails_pro/support/mock_block_helper.rb +4 -2
- data/sig/react_on_rails/generators/js_dependency_manager.rbs +123 -0
- metadata +5 -3
- data/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/TestingStreamableComponent.jsx +0 -15
|
@@ -75,26 +75,30 @@ RSpec.describe ReactOnRailsPro::LicenseValidator do
|
|
|
75
75
|
ENV["REACT_ON_RAILS_PRO_LICENSE"] = expired_token
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
context "in development/test environment" do
|
|
78
|
+
context "when in development/test environment" do
|
|
79
79
|
before do
|
|
80
80
|
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("development"))
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
it "raises error immediately" do
|
|
84
|
-
expect
|
|
84
|
+
expect do
|
|
85
|
+
described_class.validated_license_data!
|
|
86
|
+
end.to raise_error(ReactOnRailsPro::Error, /License has expired/)
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
it "includes FREE license information in error message" do
|
|
88
|
-
expect
|
|
90
|
+
expect do
|
|
91
|
+
described_class.validated_license_data!
|
|
92
|
+
end.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
|
|
89
93
|
end
|
|
90
94
|
end
|
|
91
95
|
|
|
92
|
-
context "in production environment" do
|
|
96
|
+
context "when in production environment" do
|
|
93
97
|
before do
|
|
94
98
|
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production"))
|
|
95
99
|
end
|
|
96
100
|
|
|
97
|
-
context "
|
|
101
|
+
context "with grace period (expired < 1 month ago)" do
|
|
98
102
|
let(:expired_within_grace) do
|
|
99
103
|
{
|
|
100
104
|
sub: "test@example.com",
|
|
@@ -113,7 +117,8 @@ RSpec.describe ReactOnRailsPro::LicenseValidator do
|
|
|
113
117
|
end
|
|
114
118
|
|
|
115
119
|
it "logs warning with grace period remaining" do
|
|
116
|
-
expect(mock_logger).to receive(:error)
|
|
120
|
+
expect(mock_logger).to receive(:error)
|
|
121
|
+
.with(/WARNING:.*License has expired.*Grace period:.*day\(s\) remaining/)
|
|
117
122
|
described_class.validated_license_data!
|
|
118
123
|
end
|
|
119
124
|
|
|
@@ -123,7 +128,7 @@ RSpec.describe ReactOnRailsPro::LicenseValidator do
|
|
|
123
128
|
end
|
|
124
129
|
end
|
|
125
130
|
|
|
126
|
-
context "outside grace period (expired > 1 month ago)" do
|
|
131
|
+
context "when outside grace period (expired > 1 month ago)" do
|
|
127
132
|
let(:expired_outside_grace) do
|
|
128
133
|
{
|
|
129
134
|
sub: "test@example.com",
|
|
@@ -138,11 +143,15 @@ RSpec.describe ReactOnRailsPro::LicenseValidator do
|
|
|
138
143
|
end
|
|
139
144
|
|
|
140
145
|
it "raises error" do
|
|
141
|
-
expect
|
|
146
|
+
expect do
|
|
147
|
+
described_class.validated_license_data!
|
|
148
|
+
end.to raise_error(ReactOnRailsPro::Error, /License has expired/)
|
|
142
149
|
end
|
|
143
150
|
|
|
144
151
|
it "includes FREE license information in error message" do
|
|
145
|
-
expect
|
|
152
|
+
expect do
|
|
153
|
+
described_class.validated_license_data!
|
|
154
|
+
end.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
|
|
146
155
|
end
|
|
147
156
|
end
|
|
148
157
|
end
|
|
@@ -168,7 +177,9 @@ RSpec.describe ReactOnRailsPro::LicenseValidator do
|
|
|
168
177
|
end
|
|
169
178
|
|
|
170
179
|
it "includes FREE license information in error message" do
|
|
171
|
-
expect
|
|
180
|
+
expect do
|
|
181
|
+
described_class.validated_license_data!
|
|
182
|
+
end.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
|
|
172
183
|
end
|
|
173
184
|
end
|
|
174
185
|
|
|
@@ -180,11 +191,15 @@ RSpec.describe ReactOnRailsPro::LicenseValidator do
|
|
|
180
191
|
end
|
|
181
192
|
|
|
182
193
|
it "raises error" do
|
|
183
|
-
expect
|
|
194
|
+
expect do
|
|
195
|
+
described_class.validated_license_data!
|
|
196
|
+
end.to raise_error(ReactOnRailsPro::Error, /Invalid license signature/)
|
|
184
197
|
end
|
|
185
198
|
|
|
186
199
|
it "includes FREE license information in error message" do
|
|
187
|
-
expect
|
|
200
|
+
expect do
|
|
201
|
+
described_class.validated_license_data!
|
|
202
|
+
end.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
|
|
188
203
|
end
|
|
189
204
|
end
|
|
190
205
|
|
|
@@ -194,32 +194,5 @@ describe ReactOnRailsPro::Request do
|
|
|
194
194
|
expect(mocked_block).not_to have_received(:call)
|
|
195
195
|
end
|
|
196
196
|
end
|
|
197
|
-
|
|
198
|
-
it "does not use HTTPx retries plugin for streaming requests to prevent body duplication" do
|
|
199
|
-
# This test verifies the fix for https://github.com/shakacode/react_on_rails/issues/1895
|
|
200
|
-
# When streaming requests encounter connection errors mid-transmission, HTTPx retries
|
|
201
|
-
# would cause body duplication because partial chunks are already sent to the client.
|
|
202
|
-
# The StreamRequest class handles retries properly by starting fresh requests.
|
|
203
|
-
|
|
204
|
-
# Reset connections to ensure we're using a fresh connection
|
|
205
|
-
described_class.reset_connection
|
|
206
|
-
|
|
207
|
-
# Trigger a streaming request
|
|
208
|
-
mock_streaming_response(render_full_url, 200) do |yielder|
|
|
209
|
-
yielder.call("Test chunk\n")
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
stream = described_class.render_code_as_stream("/render", "console.log('test');", is_rsc_payload: false)
|
|
213
|
-
chunks = []
|
|
214
|
-
stream.each_chunk { |chunk| chunks << chunk }
|
|
215
|
-
|
|
216
|
-
# Verify that the streaming request completed successfully
|
|
217
|
-
expect(chunks).to eq(["Test chunk"])
|
|
218
|
-
|
|
219
|
-
# Verify that the connection_without_retries was created
|
|
220
|
-
# by checking that a connection was created with retries disabled
|
|
221
|
-
connection_without_retries = described_class.send(:connection_without_retries)
|
|
222
|
-
expect(connection_without_retries).to be_a(HTTPX::Session)
|
|
223
|
-
end
|
|
224
197
|
end
|
|
225
198
|
end
|
|
@@ -15,7 +15,7 @@ require "rails"
|
|
|
15
15
|
require "rails/test_help"
|
|
16
16
|
Rails.backtrace_cleaner.remove_silencers!
|
|
17
17
|
|
|
18
|
-
require_relative "
|
|
18
|
+
require_relative "simplecov_helper"
|
|
19
19
|
# prevent Test::Unit's AutoRunner from executing during RSpec's rake task
|
|
20
20
|
Test::Unit.run = true if defined?(Test::Unit) && Test::Unit.respond_to?(:run=)
|
|
21
21
|
|
|
@@ -62,4 +62,93 @@ RSpec.describe ReactOnRailsPro::StreamDecorator do
|
|
|
62
62
|
expect(chunks.last).to end_with("-end")
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
|
+
|
|
66
|
+
describe "#rescue" do
|
|
67
|
+
it "catches the error happens inside the component" do
|
|
68
|
+
allow(mock_component).to receive(:each_chunk).and_raise(StandardError.new("Fake Error"))
|
|
69
|
+
mocked_block = mock_block
|
|
70
|
+
|
|
71
|
+
stream_decorator.rescue(&mocked_block.block)
|
|
72
|
+
chunks = []
|
|
73
|
+
expect { stream_decorator.each_chunk { |chunk| chunks << chunk } }.not_to raise_error
|
|
74
|
+
|
|
75
|
+
expect(mocked_block).to have_received(:call) do |error|
|
|
76
|
+
expect(error).to be_a(StandardError)
|
|
77
|
+
expect(error.message).to eq("Fake Error")
|
|
78
|
+
end
|
|
79
|
+
expect(chunks).to eq([])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "catches the error happens inside subsequent component calls" do
|
|
83
|
+
allow(mock_component).to receive(:each_chunk).and_yield("Chunk1").and_raise(ArgumentError.new("Fake Error"))
|
|
84
|
+
mocked_block = mock_block
|
|
85
|
+
|
|
86
|
+
stream_decorator.rescue(&mocked_block.block)
|
|
87
|
+
chunks = []
|
|
88
|
+
expect { stream_decorator.each_chunk { |chunk| chunks << chunk } }.not_to raise_error
|
|
89
|
+
|
|
90
|
+
expect(mocked_block).to have_received(:call) do |error|
|
|
91
|
+
expect(chunks).to eq(["Chunk1"])
|
|
92
|
+
expect(error).to be_a(ArgumentError)
|
|
93
|
+
expect(error.message).to eq("Fake Error")
|
|
94
|
+
end
|
|
95
|
+
expect(chunks).to eq(["Chunk1"])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "can yield values to the stream" do
|
|
99
|
+
allow(mock_component).to receive(:each_chunk).and_yield("Chunk1").and_raise(ArgumentError.new("Fake Error"))
|
|
100
|
+
mocked_block = mock_block
|
|
101
|
+
|
|
102
|
+
stream_decorator.rescue(&mocked_block.block)
|
|
103
|
+
chunks = []
|
|
104
|
+
expect { stream_decorator.each_chunk { |chunk| chunks << chunk } }.not_to raise_error
|
|
105
|
+
|
|
106
|
+
expect(mocked_block).to have_received(:call) do |error, &inner_block|
|
|
107
|
+
expect(chunks).to eq(["Chunk1"])
|
|
108
|
+
expect(error).to be_a(ArgumentError)
|
|
109
|
+
expect(error.message).to eq("Fake Error")
|
|
110
|
+
|
|
111
|
+
inner_block.call "Chunk from rescue block"
|
|
112
|
+
inner_block.call "Chunk2 from rescue block"
|
|
113
|
+
end
|
|
114
|
+
expect(chunks).to eq(["Chunk1", "Chunk from rescue block", "Chunk2 from rescue block"])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "can convert the error into another error" do
|
|
118
|
+
allow(mock_component).to receive(:each_chunk).and_raise(StandardError.new("Fake Error"))
|
|
119
|
+
mocked_block = mock_block do |error|
|
|
120
|
+
expect(error).to be_a(StandardError)
|
|
121
|
+
expect(error.message).to eq("Fake Error")
|
|
122
|
+
raise ArgumentError, "Another Error"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
stream_decorator.rescue(&mocked_block.block)
|
|
126
|
+
chunks = []
|
|
127
|
+
expect { stream_decorator.each_chunk { |chunk| chunks << chunk } }.to raise_error(ArgumentError, "Another Error")
|
|
128
|
+
expect(chunks).to eq([])
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "chains multiple rescue blocks" do
|
|
132
|
+
allow(mock_component).to receive(:each_chunk).and_yield("Chunk1").and_raise(StandardError.new("Fake Error"))
|
|
133
|
+
fist_rescue_block = mock_block do |error, &block|
|
|
134
|
+
expect(error).to be_a(StandardError)
|
|
135
|
+
expect(error.message).to eq("Fake Error")
|
|
136
|
+
block.call "Chunk from first rescue block"
|
|
137
|
+
raise ArgumentError, "Another Error"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
second_rescue_block = mock_block do |error, &block|
|
|
141
|
+
expect(error).to be_a(ArgumentError)
|
|
142
|
+
expect(error.message).to eq("Another Error")
|
|
143
|
+
block.call "Chunk from second rescue block"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
stream_decorator.rescue(&fist_rescue_block.block)
|
|
147
|
+
stream_decorator.rescue(&second_rescue_block.block)
|
|
148
|
+
chunks = []
|
|
149
|
+
expect { stream_decorator.each_chunk { |chunk| chunks << chunk } }.not_to raise_error
|
|
150
|
+
|
|
151
|
+
expect(chunks).to eq(["Chunk1", "Chunk from first rescue block", "Chunk from second rescue block"])
|
|
152
|
+
end
|
|
153
|
+
end
|
|
65
154
|
end
|
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "async"
|
|
4
|
+
require "async/queue"
|
|
3
5
|
require_relative "spec_helper"
|
|
4
6
|
|
|
7
|
+
class StreamController
|
|
8
|
+
include ReactOnRailsPro::Stream
|
|
9
|
+
|
|
10
|
+
attr_reader :response
|
|
11
|
+
|
|
12
|
+
def initialize(component_queues:, initial_response: "TEMPLATE")
|
|
13
|
+
@component_queues = component_queues
|
|
14
|
+
@initial_response = initial_response
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def render_to_string(**_opts)
|
|
18
|
+
@rorp_rendering_fibers = @component_queues.map do |queue|
|
|
19
|
+
Fiber.new do
|
|
20
|
+
loop do
|
|
21
|
+
chunk = queue.dequeue
|
|
22
|
+
break if chunk.nil?
|
|
23
|
+
|
|
24
|
+
Fiber.yield chunk
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@initial_response
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
5
33
|
RSpec.describe "Streaming API" do
|
|
6
34
|
let(:origin) { "http://api.example.com" }
|
|
7
35
|
let(:path) { "/stream" }
|
|
@@ -342,4 +370,120 @@ RSpec.describe "Streaming API" do
|
|
|
342
370
|
expect(mocked_block).to have_received(:call).with("First chunk")
|
|
343
371
|
end
|
|
344
372
|
end
|
|
373
|
+
|
|
374
|
+
describe "Component streaming concurrency" do
|
|
375
|
+
def run_stream(controller, template: "ignored")
|
|
376
|
+
Sync do |parent|
|
|
377
|
+
parent.async { controller.stream_view_containing_react_components(template: template) }
|
|
378
|
+
yield(parent)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def setup_stream_test(component_count: 2)
|
|
383
|
+
component_queues = Array.new(component_count) { Async::Queue.new }
|
|
384
|
+
controller = StreamController.new(component_queues: component_queues)
|
|
385
|
+
|
|
386
|
+
mocked_response = instance_double(ActionController::Live::Response)
|
|
387
|
+
mocked_stream = instance_double(ActionController::Live::Buffer)
|
|
388
|
+
allow(mocked_response).to receive(:stream).and_return(mocked_stream)
|
|
389
|
+
allow(mocked_stream).to receive(:write)
|
|
390
|
+
allow(mocked_stream).to receive(:close)
|
|
391
|
+
allow(controller).to receive(:response).and_return(mocked_response)
|
|
392
|
+
|
|
393
|
+
[component_queues, controller, mocked_stream]
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
it "streams components concurrently" do
|
|
397
|
+
queues, controller, stream = setup_stream_test
|
|
398
|
+
|
|
399
|
+
run_stream(controller) do |_parent|
|
|
400
|
+
queues[1].enqueue("B1")
|
|
401
|
+
sleep 0.05
|
|
402
|
+
expect(stream).to have_received(:write).with("B1")
|
|
403
|
+
|
|
404
|
+
queues[0].enqueue("A1")
|
|
405
|
+
sleep 0.05
|
|
406
|
+
expect(stream).to have_received(:write).with("A1")
|
|
407
|
+
|
|
408
|
+
queues[1].enqueue("B2")
|
|
409
|
+
queues[1].close
|
|
410
|
+
sleep 0.05
|
|
411
|
+
|
|
412
|
+
queues[0].enqueue("A2")
|
|
413
|
+
queues[0].close
|
|
414
|
+
sleep 0.1
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
it "maintains per-component ordering" do
|
|
419
|
+
queues, controller, stream = setup_stream_test
|
|
420
|
+
|
|
421
|
+
run_stream(controller) do |_parent|
|
|
422
|
+
queues[0].enqueue("X1")
|
|
423
|
+
queues[0].enqueue("X2")
|
|
424
|
+
queues[0].enqueue("X3")
|
|
425
|
+
queues[0].close
|
|
426
|
+
|
|
427
|
+
queues[1].enqueue("Y1")
|
|
428
|
+
queues[1].enqueue("Y2")
|
|
429
|
+
queues[1].close
|
|
430
|
+
|
|
431
|
+
sleep 0.2
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Verify all chunks were written
|
|
435
|
+
expect(stream).to have_received(:write).with("X1")
|
|
436
|
+
expect(stream).to have_received(:write).with("X2")
|
|
437
|
+
expect(stream).to have_received(:write).with("X3")
|
|
438
|
+
expect(stream).to have_received(:write).with("Y1")
|
|
439
|
+
expect(stream).to have_received(:write).with("Y2")
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
it "handles empty component list" do
|
|
443
|
+
_queues, controller, stream = setup_stream_test(component_count: 0)
|
|
444
|
+
|
|
445
|
+
run_stream(controller) do |_parent|
|
|
446
|
+
sleep 0.1
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
expect(stream).to have_received(:write).with("TEMPLATE")
|
|
450
|
+
expect(stream).to have_received(:close)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
it "handles single component" do
|
|
454
|
+
queues, controller, stream = setup_stream_test(component_count: 1)
|
|
455
|
+
|
|
456
|
+
run_stream(controller) do |_parent|
|
|
457
|
+
queues[0].enqueue("Single1")
|
|
458
|
+
queues[0].enqueue("Single2")
|
|
459
|
+
queues[0].close
|
|
460
|
+
|
|
461
|
+
sleep 0.1
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
expect(stream).to have_received(:write).with("Single1")
|
|
465
|
+
expect(stream).to have_received(:write).with("Single2")
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
it "applies backpressure with slow writer" do
|
|
469
|
+
queues, controller, stream = setup_stream_test(component_count: 1)
|
|
470
|
+
|
|
471
|
+
write_timestamps = []
|
|
472
|
+
allow(stream).to receive(:write) do |_data|
|
|
473
|
+
write_timestamps << Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
474
|
+
sleep 0.05
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
run_stream(controller) do |_parent|
|
|
478
|
+
5.times { |i| queues[0].enqueue("Chunk#{i}") }
|
|
479
|
+
queues[0].close
|
|
480
|
+
|
|
481
|
+
sleep 1
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
expect(write_timestamps.length).to be >= 2
|
|
485
|
+
gaps = write_timestamps.each_cons(2).map { |a, b| b - a }
|
|
486
|
+
expect(gaps.all? { |gap| gap >= 0.04 }).to be true
|
|
487
|
+
end
|
|
488
|
+
end
|
|
345
489
|
end
|
|
@@ -4,7 +4,7 @@ RSpec.configure do |config|
|
|
|
4
4
|
config.before(:each, :caching) do
|
|
5
5
|
cache_store = ActiveSupport::Cache::MemoryStore.new
|
|
6
6
|
allow(controller).to receive(:cache_store).and_return(cache_store) if defined?(controller) && controller
|
|
7
|
-
allow(
|
|
7
|
+
allow(Rails).to receive(:cache).and_return(cache_store)
|
|
8
8
|
ReactOnRailsPro::Cache.instance_variable_set(:@serializer_checksum, nil)
|
|
9
9
|
Rails.cache.clear
|
|
10
10
|
end
|
|
@@ -9,9 +9,11 @@ module MockBlockHelper
|
|
|
9
9
|
# mocked_block = mock_block
|
|
10
10
|
# testing_method_taking_block(&mocked_block.block)
|
|
11
11
|
# expect(mocked_block).to have_received(:call).with(1, 2, 3)
|
|
12
|
-
def mock_block(
|
|
12
|
+
def mock_block(&block)
|
|
13
13
|
double("BlockMock").tap do |mock| # rubocop:disable RSpec/VerifiedDoubles
|
|
14
|
-
allow(mock).to receive(:call)
|
|
14
|
+
allow(mock).to receive(:call) do |*args, &inner_block|
|
|
15
|
+
block&.call(*args, &inner_block)
|
|
16
|
+
end
|
|
15
17
|
def mock.block
|
|
16
18
|
method(:call).to_proc
|
|
17
19
|
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReactOnRails
|
|
4
|
+
module Generators
|
|
5
|
+
# Type signatures for JsDependencyManager module
|
|
6
|
+
#
|
|
7
|
+
# This module provides common functionality for managing JavaScript dependencies
|
|
8
|
+
# in Rails generators using the package_json gem (available via shakapacker).
|
|
9
|
+
module JsDependencyManager
|
|
10
|
+
# Core React dependencies required for React on Rails
|
|
11
|
+
REACT_DEPENDENCIES: Array[String]
|
|
12
|
+
|
|
13
|
+
# CSS processing dependencies for webpack
|
|
14
|
+
CSS_DEPENDENCIES: Array[String]
|
|
15
|
+
|
|
16
|
+
# Development-only dependencies for hot reloading (Webpack)
|
|
17
|
+
DEV_DEPENDENCIES: Array[String]
|
|
18
|
+
|
|
19
|
+
# Rspack core dependencies (only installed when --rspack flag is used)
|
|
20
|
+
RSPACK_DEPENDENCIES: Array[String]
|
|
21
|
+
|
|
22
|
+
# Rspack development dependencies for hot reloading
|
|
23
|
+
RSPACK_DEV_DEPENDENCIES: Array[String]
|
|
24
|
+
|
|
25
|
+
# TypeScript dependencies (only installed when --typescript flag is used)
|
|
26
|
+
TYPESCRIPT_DEPENDENCIES: Array[String]
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Sets up JavaScript dependencies by adding and installing packages
|
|
31
|
+
#
|
|
32
|
+
# This method orchestrates the entire dependency setup process:
|
|
33
|
+
# 1. Adds all required packages to package.json
|
|
34
|
+
# 2. Runs package manager install
|
|
35
|
+
#
|
|
36
|
+
# @return [void]
|
|
37
|
+
def setup_js_dependencies: () -> void
|
|
38
|
+
|
|
39
|
+
# Adds all JavaScript dependencies to package.json
|
|
40
|
+
#
|
|
41
|
+
# This method calls individual add_*_dependencies methods in sequence.
|
|
42
|
+
# All errors are handled gracefully with warnings rather than exceptions.
|
|
43
|
+
#
|
|
44
|
+
# @return [void]
|
|
45
|
+
def add_js_dependencies: () -> void
|
|
46
|
+
|
|
47
|
+
# Adds the react-on-rails package to package.json
|
|
48
|
+
#
|
|
49
|
+
# Uses version matching for stable releases, or latest for pre-releases.
|
|
50
|
+
# Adds error message to GeneratorMessages if package addition fails.
|
|
51
|
+
#
|
|
52
|
+
# @return [void]
|
|
53
|
+
def add_react_on_rails_package: () -> void
|
|
54
|
+
|
|
55
|
+
# Adds React dependencies to package.json
|
|
56
|
+
#
|
|
57
|
+
# Adds error message to GeneratorMessages if package addition fails.
|
|
58
|
+
#
|
|
59
|
+
# @return [void]
|
|
60
|
+
def add_react_dependencies: () -> void
|
|
61
|
+
|
|
62
|
+
# Adds CSS processing dependencies to package.json
|
|
63
|
+
#
|
|
64
|
+
# Adds error message to GeneratorMessages if package addition fails.
|
|
65
|
+
#
|
|
66
|
+
# @return [void]
|
|
67
|
+
def add_css_dependencies: () -> void
|
|
68
|
+
|
|
69
|
+
# Adds Rspack dependencies to package.json
|
|
70
|
+
#
|
|
71
|
+
# Only called when --rspack flag is set.
|
|
72
|
+
# Adds error message to GeneratorMessages if package addition fails.
|
|
73
|
+
#
|
|
74
|
+
# @return [void]
|
|
75
|
+
def add_rspack_dependencies: () -> void
|
|
76
|
+
|
|
77
|
+
# Adds TypeScript dependencies to package.json as dev dependencies
|
|
78
|
+
#
|
|
79
|
+
# Only called when --typescript flag is set.
|
|
80
|
+
# Adds error message to GeneratorMessages if package addition fails.
|
|
81
|
+
#
|
|
82
|
+
# @return [void]
|
|
83
|
+
def add_typescript_dependencies: () -> void
|
|
84
|
+
|
|
85
|
+
# Adds development dependencies to package.json
|
|
86
|
+
#
|
|
87
|
+
# Chooses between Webpack or Rspack dev dependencies based on --rspack flag.
|
|
88
|
+
# Adds error message to GeneratorMessages if package addition fails.
|
|
89
|
+
#
|
|
90
|
+
# @return [void]
|
|
91
|
+
def add_dev_dependencies: () -> void
|
|
92
|
+
|
|
93
|
+
# Adds a single package using package_json gem
|
|
94
|
+
#
|
|
95
|
+
# This method is used internally for adding the react-on-rails package
|
|
96
|
+
# with version-specific handling (react-on-rails@VERSION).
|
|
97
|
+
# For batch operations, use add_packages instead.
|
|
98
|
+
#
|
|
99
|
+
# @param package [String] Package specifier (e.g., "react-on-rails@16.0.0")
|
|
100
|
+
# @param dev [bool] Whether to add as dev dependency
|
|
101
|
+
# @return [bool] true if successful, false otherwise
|
|
102
|
+
def add_package: (String package, ?dev: bool) -> bool
|
|
103
|
+
|
|
104
|
+
# Adds multiple packages at once using package_json gem
|
|
105
|
+
#
|
|
106
|
+
# Delegates to GeneratorHelper's add_npm_dependencies for better
|
|
107
|
+
# package manager abstraction and batch processing efficiency.
|
|
108
|
+
#
|
|
109
|
+
# @param packages [Array<String>] Package names to add
|
|
110
|
+
# @param dev [bool] Whether to add as dev dependencies
|
|
111
|
+
# @return [bool] true if successful, false otherwise
|
|
112
|
+
def add_packages: (Array[String] packages, ?dev: bool) -> bool
|
|
113
|
+
|
|
114
|
+
# Installs JavaScript dependencies using package_json gem
|
|
115
|
+
#
|
|
116
|
+
# Always available via shakapacker dependency chain.
|
|
117
|
+
# Adds warning to GeneratorMessages if installation fails.
|
|
118
|
+
#
|
|
119
|
+
# @return [bool] true if successful, false otherwise
|
|
120
|
+
def install_js_dependencies: () -> bool
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: react_on_rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 16.2.0.beta.
|
|
4
|
+
version: 16.2.0.beta.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Gordon
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-11-
|
|
11
|
+
date: 2025-11-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: addressable
|
|
@@ -167,6 +167,7 @@ files:
|
|
|
167
167
|
- lib/generators/react_on_rails/generator_helper.rb
|
|
168
168
|
- lib/generators/react_on_rails/generator_messages.rb
|
|
169
169
|
- lib/generators/react_on_rails/install_generator.rb
|
|
170
|
+
- lib/generators/react_on_rails/js_dependency_manager.rb
|
|
170
171
|
- lib/generators/react_on_rails/react_no_redux_generator.rb
|
|
171
172
|
- lib/generators/react_on_rails/react_with_redux_generator.rb
|
|
172
173
|
- lib/generators/react_on_rails/templates/.eslintrc
|
|
@@ -606,6 +607,7 @@ files:
|
|
|
606
607
|
- react_on_rails_pro/spec/dummy/bin/setup
|
|
607
608
|
- react_on_rails_pro/spec/dummy/bin/shakapacker
|
|
608
609
|
- react_on_rails_pro/spec/dummy/bin/shakapacker-dev-server
|
|
610
|
+
- react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
|
|
609
611
|
- react_on_rails_pro/spec/dummy/bin/spring
|
|
610
612
|
- react_on_rails_pro/spec/dummy/bin/sprockets
|
|
611
613
|
- react_on_rails_pro/spec/dummy/bin/term_display
|
|
@@ -756,7 +758,6 @@ files:
|
|
|
756
758
|
- react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/SetTimeoutLoggingApp.server.jsx
|
|
757
759
|
- react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/SimpleComponent.jsx
|
|
758
760
|
- react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/StreamAsyncComponents.jsx
|
|
759
|
-
- react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/TestingStreamableComponent.jsx
|
|
760
761
|
- react_on_rails_pro/spec/dummy/client/app/routes/routes.jsx
|
|
761
762
|
- react_on_rails_pro/spec/dummy/client/app/ssr-computations/userQuery.ssr-computation.ts
|
|
762
763
|
- react_on_rails_pro/spec/dummy/client/app/store/composeInitialState.js
|
|
@@ -974,6 +975,7 @@ files:
|
|
|
974
975
|
- sig/react_on_rails/configuration.rbs
|
|
975
976
|
- sig/react_on_rails/controller.rbs
|
|
976
977
|
- sig/react_on_rails/error.rbs
|
|
978
|
+
- sig/react_on_rails/generators/js_dependency_manager.rbs
|
|
977
979
|
- sig/react_on_rails/git_utils.rbs
|
|
978
980
|
- sig/react_on_rails/helper.rbs
|
|
979
981
|
- sig/react_on_rails/json_parse_error.rbs
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
|
|
5
|
-
// Simple test component for streaming SSR tests
|
|
6
|
-
function TestingStreamableComponent({ helloWorldData }) {
|
|
7
|
-
return (
|
|
8
|
-
<div>
|
|
9
|
-
<div>Chunk 1: Stream React Server Components</div>
|
|
10
|
-
<div>Hello, {helloWorldData.name}!</div>
|
|
11
|
-
</div>
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export default TestingStreamableComponent;
|