ruact 0.0.11 → 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 +110 -48
  3. data/CONTRIBUTING.md +233 -0
  4. data/README.md +14 -4
  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 -12
  24. data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -109
  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,105 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Story 9.4 — unit spec for the Ruact::Query base class. Deliberately NO Rails
4
- # boot (AC3): a query subclass is exercised with a plain double standing in for
5
- # the dispatch context, proving `CatalogQuery.new(fake_context).categories` is
6
- # unit-testable in isolation.
7
- require "spec_helper"
8
-
9
- module Ruact
10
- RSpec.describe Query, :story_9_4 do
11
- let(:fake_user) { { "id" => 42 } }
12
- let(:fake_params) { { "q" => "ruby" } }
13
- let(:fake_request) { instance_double(Object) }
14
- let(:fake_session) { { "token" => "abc" } }
15
- let(:context) do
16
- double(
17
- current_user: fake_user,
18
- params: fake_params,
19
- request: fake_request,
20
- session: fake_session
21
- )
22
- end
23
-
24
- let(:query_class) do
25
- Class.new(described_class) do
26
- def categories
27
- %w[books games]
28
- end
29
-
30
- def whoami
31
- current_user
32
- end
33
-
34
- def search
35
- params["q"]
36
- end
37
- end
38
- end
39
-
40
- describe "Story 9.4 — context accessors delegate to the injected context (AC3)" do
41
- subject(:query) { query_class.new(context) }
42
-
43
- it "exposes current_user from the context" do
44
- expect(query.whoami).to eq(fake_user)
45
- end
46
-
47
- it "exposes params from the context" do
48
- expect(query.search).to eq("ruby")
49
- end
50
-
51
- it "exposes request from the context" do
52
- expect(query.request).to be(fake_request)
53
- end
54
-
55
- it "exposes session from the context" do
56
- expect(query.session).to eq(fake_session)
57
- end
58
-
59
- it "is unit-testable with a plain fake context and no Rails boot (AC3)" do
60
- expect(query_class.new(context).categories).to eq(%w[books games])
61
- end
62
- end
63
-
64
- describe "Story 9.4 — accessor methods are inherited, never own methods of the subclass (AC1)" do
65
- it "keeps current_user/params/request/session OUT of the subclass's public_instance_methods(false)" do
66
- own = query_class.public_instance_methods(false)
67
- expect(own).to contain_exactly(:categories, :whoami, :search)
68
- end
69
-
70
- it "defines the accessors on Ruact::Query itself (inherited by every subclass)" do
71
- expect(described_class.public_instance_methods(false))
72
- .to include(:current_user, :params, :request, :session)
73
- end
74
- end
75
-
76
- describe "Story 9.4 — ruact_skip_before_action class macro (AC4 / D1)" do
77
- it "records the callback with its options on the query class" do
78
- klass = Class.new(described_class)
79
- klass.ruact_skip_before_action(:require_login, only: :categories)
80
- expect(klass.__ruact_skipped_callbacks).to eq([[[:require_login], { only: :categories }]])
81
- end
82
-
83
- it "accepts multiple callbacks in one call, mirroring Rails' skip_before_action" do
84
- klass = Class.new(described_class)
85
- klass.ruact_skip_before_action(:require_login, :check_tenant)
86
- expect(klass.__ruact_skipped_callbacks).to eq([[%i[require_login check_tenant], {}]])
87
- end
88
-
89
- it "accumulates across calls in declaration order" do
90
- klass = Class.new(described_class)
91
- klass.ruact_skip_before_action(:require_login)
92
- klass.ruact_skip_before_action(:check_tenant, raise: false)
93
- expect(klass.__ruact_skipped_callbacks)
94
- .to eq([[[:require_login], {}], [[:check_tenant], { raise: false }]])
95
- end
96
-
97
- it "keeps the recorded skips per-class — sibling query classes never share them" do
98
- klass_a = Class.new(described_class)
99
- klass_b = Class.new(described_class)
100
- klass_a.ruact_skip_before_action(:require_login)
101
- expect(klass_b.__ruact_skipped_callbacks).to be_empty
102
- end
103
- end
104
- end
105
- end
@@ -1,155 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "pathname"
5
- require "logger"
6
- require "socket"
7
-
8
- # The Rails stub is provided by spec/support/rails_stub.rb.
9
- # We explicitly require the railtie here (was skipped in ruact.rb because
10
- # Rails was not defined when that file loaded).
11
- require "ruact/railtie"
12
-
13
- RSpec.describe Ruact::Railtie do
14
- let(:missing_path) { Pathname.new("/nonexistent/path/react-client-manifest.json") }
15
- let(:fake_logger) { instance_double(Logger, warn: nil, info: nil) }
16
-
17
- before do
18
- Rails.logger = fake_logger
19
- end
20
-
21
- describe ".detect_streaming_mode! (AC#1–3)" do
22
- after { Ruact.streaming_mode = nil }
23
-
24
- context "when Puma is defined" do
25
- before do
26
- puma = Module.new { const_set(:Server, Class.new) }
27
- stub_const("Puma", puma)
28
- end
29
-
30
- it "sets streaming_mode to :enabled" do
31
- described_class.detect_streaming_mode!
32
- expect(Ruact.streaming_mode).to eq(:enabled)
33
- end
34
-
35
- it "logs streaming: enabled (Puma detected)" do
36
- described_class.detect_streaming_mode!
37
- expect(fake_logger).to have_received(:info)
38
- .with(a_string_including("streaming: enabled (Puma detected)"))
39
- end
40
- end
41
-
42
- context "when Unicorn is defined" do
43
- before { stub_const("Unicorn", Module.new) }
44
-
45
- it "sets streaming_mode to :buffered" do
46
- described_class.detect_streaming_mode!
47
- expect(Ruact.streaming_mode).to eq(:buffered)
48
- end
49
-
50
- it "logs streaming: buffered (Unicorn detected)" do
51
- described_class.detect_streaming_mode!
52
- expect(fake_logger).to have_received(:info)
53
- .with(a_string_including("streaming: buffered (Unicorn detected)"))
54
- end
55
- end
56
-
57
- context "when PhusionPassenger is defined" do
58
- before { stub_const("PhusionPassenger", Module.new) }
59
-
60
- it "sets streaming_mode to :buffered" do
61
- described_class.detect_streaming_mode!
62
- expect(Ruact.streaming_mode).to eq(:buffered)
63
- end
64
-
65
- it "logs streaming: buffered (Passenger detected)" do
66
- described_class.detect_streaming_mode!
67
- expect(fake_logger).to have_received(:info)
68
- .with(a_string_including("streaming: buffered (Passenger detected)"))
69
- end
70
- end
71
-
72
- context "when no recognized server constant is defined" do
73
- it "sets streaming_mode to :buffered" do
74
- described_class.detect_streaming_mode!
75
- expect(Ruact.streaming_mode).to eq(:buffered)
76
- end
77
-
78
- it "logs server unknown — defaulting to safe mode" do
79
- described_class.detect_streaming_mode!
80
- expect(fake_logger).to have_received(:info)
81
- .with(a_string_including("server unknown — defaulting to safe mode"))
82
- end
83
- end
84
- end
85
-
86
- describe ".check_manifest!" do
87
- context "with missing manifest in development (AC#5, #7)" do
88
- before do
89
- Rails.env = ActiveSupport::StringInquirer.new("development")
90
- end
91
-
92
- it "does not raise" do
93
- expect { described_class.check_manifest!(missing_path) }.not_to raise_error
94
- end
95
-
96
- it "logs a [ruact] prefixed warning" do
97
- described_class.check_manifest!(missing_path)
98
- expect(fake_logger).to have_received(:warn)
99
- .with(a_string_starting_with("[ruact]"))
100
- end
101
-
102
- it "includes the manifest path in the warning" do
103
- described_class.check_manifest!(missing_path)
104
- expect(fake_logger).to have_received(:warn)
105
- .with(a_string_including(missing_path.to_s))
106
- end
107
- end
108
-
109
- context "with missing manifest in production (AC#6)" do
110
- before { Rails.env = ActiveSupport::StringInquirer.new("production") }
111
- after { Rails.env = ActiveSupport::StringInquirer.new("development") }
112
-
113
- it "raises ManifestError" do
114
- expect { described_class.check_manifest!(missing_path) }
115
- .to raise_error(Ruact::ManifestError)
116
- end
117
-
118
- it "error message contains 'run vite build before deploying'" do
119
- expect { described_class.check_manifest!(missing_path) }
120
- .to raise_error(Ruact::ManifestError, /run vite build before deploying/)
121
- end
122
- end
123
- end
124
-
125
- describe ".check_vite!" do
126
- context "when Vite is running (AC#4)" do
127
- before do
128
- allow(TCPSocket).to receive(:new).and_return(instance_double(TCPSocket, close: nil))
129
- end
130
-
131
- it "does not log a warning" do
132
- described_class.check_vite!
133
- expect(fake_logger).not_to have_received(:warn)
134
- end
135
- end
136
-
137
- context "when Vite is not running (AC#4)" do
138
- before do
139
- allow(TCPSocket).to receive(:new).and_raise(Errno::ECONNREFUSED)
140
- end
141
-
142
- it "logs a [ruact] prefixed warning" do
143
- described_class.check_vite!
144
- expect(fake_logger).to have_received(:warn)
145
- .with(a_string_starting_with("[ruact]"))
146
- end
147
-
148
- it "mentions Vite and port 5173" do
149
- described_class.check_vite!
150
- expect(fake_logger).to have_received(:warn)
151
- .with(a_string_including("localhost:5173"))
152
- end
153
- end
154
- end
155
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- module Ruact
6
- RSpec.describe RenderContext do
7
- subject(:ctx) { described_class.new }
8
-
9
- describe "#register" do
10
- it "appends a new component entry" do
11
- ctx.register("NavBar", { "currentUser" => 1 })
12
- expect(ctx.components.length).to eq(1)
13
- expect(ctx.components.first[:name]).to eq("NavBar")
14
- expect(ctx.components.first[:props]).to eq({ "currentUser" => 1 })
15
- end
16
-
17
- it "returns a token of the form __RUACT_<index>__" do
18
- token = ctx.register("Foo", {})
19
- expect(token).to eq("__RUACT_0__")
20
- end
21
-
22
- it "increments token indices across successive registrations" do
23
- t0 = ctx.register("A", {})
24
- t1 = ctx.register("B", {})
25
- t2 = ctx.register("C", {})
26
- expect([t0, t1, t2]).to eq(%w[__RUACT_0__ __RUACT_1__ __RUACT_2__])
27
- end
28
- end
29
-
30
- describe "#components" do
31
- it "starts empty" do
32
- expect(ctx.components).to eq([])
33
- end
34
- end
35
-
36
- describe "#by_token" do
37
- it "finds a registered component by its token" do
38
- ctx.register("NavBar", { "x" => 1 })
39
- entry = ctx.by_token("__RUACT_0__")
40
- expect(entry[:name]).to eq("NavBar")
41
- end
42
-
43
- it "returns nil for an unknown token" do
44
- expect(ctx.by_token("__RUACT_99__")).to be_nil
45
- end
46
- end
47
-
48
- describe "isolation" do
49
- it "two contexts are independent" do
50
- a = described_class.new
51
- b = described_class.new
52
- a.register("A", {})
53
- expect(b.components).to be_empty
54
- expect(a.components.length).to eq(1)
55
- end
56
- end
57
- end
58
- end
@@ -1,78 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- module Ruact
6
- RSpec.describe "Concurrent render isolation" do
7
- let(:manifest) do
8
- ClientManifest.from_hash(
9
- (0..7).to_h do |i|
10
- ["ThreadComponent#{i}", { "id" => "/tc#{i}.js",
11
- "name" => "ThreadComponent#{i}",
12
- "chunks" => ["/tc#{i}.js"] }]
13
- end
14
- )
15
- end
16
-
17
- let(:pipeline) { RenderPipeline.new(manifest) }
18
-
19
- # Per Story 7.1 AC4: prove thread isolation deterministically with a
20
- # countdown latch (Mutex+Queue) so all threads arrive at the render call
21
- # simultaneously, fixed seed for reproducibility, ≥4 threads to outnumber
22
- # typical CI cores, and ≥100 iterations to expose any race that fires
23
- # ≥1% of the time.
24
- let(:thread_count) { 4 }
25
- let(:iterations) { 100 }
26
-
27
- def run_isolation_iteration(iter, count, pipeline)
28
- srand(0xC0DE + iter)
29
- ready_latch = Queue.new
30
- release = Queue.new
31
- results = Array.new(count)
32
- results_mu = Mutex.new
33
-
34
- threads = Array.new(count) do |tid|
35
- Thread.new do
36
- ctx = Object.new
37
- ctx.instance_variable_set(:@tid, tid)
38
- binding_ctx = ctx.instance_eval { binding }
39
- ready_latch << :ready
40
- release.pop
41
- output = pipeline.render(
42
- { erb: "<ThreadComponent#{tid} thread_id={@tid} />", binding: binding_ctx },
43
- mode: :string
44
- )
45
- results_mu.synchronize { results[tid] = output }
46
- end
47
- end
48
-
49
- count.times { ready_latch.pop }
50
- count.times { release << :go }
51
- threads.each(&:join)
52
- results
53
- end
54
-
55
- it "isolates each render's component registry from other concurrent renders" do
56
- iterations.times do |iter|
57
- results = run_isolation_iteration(iter, thread_count, pipeline)
58
-
59
- results.each_with_index do |output, tid|
60
- expect(output).to include_flight_row(
61
- class: :import, payload: array_including("ThreadComponent#{tid}")
62
- ), "iter #{iter} thread #{tid}: missing own component import"
63
- expect(output).to include_flight_row(
64
- class: :model, payload: array_including(hash_including("thread_id" => tid))
65
- ), "iter #{iter} thread #{tid}: missing own thread_id prop"
66
-
67
- (0...thread_count).each do |other_tid|
68
- next if other_tid == tid
69
-
70
- expect(output).not_to include_flight_row(
71
- class: :import, payload: array_including("ThreadComponent#{other_tid}")
72
- ), "iter #{iter} thread #{tid} leaked ThreadComponent#{other_tid}"
73
- end
74
- end
75
- end
76
- end
77
- end
78
- end