ruact 0.0.5 → 0.0.7

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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +54 -2
  3. data/.rubocop_todo.yml +3 -115
  4. data/CHANGELOG.md +74 -18
  5. data/bench/server_functions_dispatch_bench.rb +109 -142
  6. data/bench/server_functions_dispatch_bench.results.md +29 -0
  7. data/docs/internal/decisions/server-functions-api.md +402 -0
  8. data/lib/generators/ruact/install/install_generator.rb +310 -25
  9. data/lib/generators/ruact/install/templates/Procfile.dev.tt +2 -0
  10. data/lib/generators/ruact/install/templates/dev.tt +16 -0
  11. data/lib/generators/ruact/install/templates/package.json.tt +17 -0
  12. data/lib/generators/ruact/install/templates/vite.config.js.tt +3 -1
  13. data/lib/generators/ruact/scaffold/scaffold_attribute.rb +114 -0
  14. data/lib/generators/ruact/scaffold/scaffold_form_helpers.rb +114 -0
  15. data/lib/generators/ruact/scaffold/scaffold_generator.rb +491 -0
  16. data/lib/generators/ruact/scaffold/scaffold_shadcn_preflight.rb +229 -0
  17. data/lib/generators/ruact/scaffold/templates/components/DeleteDialog.tsx.tt +104 -0
  18. data/lib/generators/ruact/scaffold/templates/components/Form.tsx.tt +212 -0
  19. data/lib/generators/ruact/scaffold/templates/components/List.tsx.tt +338 -0
  20. data/lib/generators/ruact/scaffold/templates/components/agnostic/DeleteDialog.tsx.tt +92 -0
  21. data/lib/generators/ruact/scaffold/templates/components/agnostic/Form.tsx.tt +174 -0
  22. data/lib/generators/ruact/scaffold/templates/components/agnostic/List.tsx.tt +253 -0
  23. data/lib/generators/ruact/scaffold/templates/controller.rb.tt +130 -0
  24. data/lib/generators/ruact/scaffold/templates/queries/application_query.rb.tt +13 -0
  25. data/lib/generators/ruact/scaffold/templates/queries/query.rb.tt +59 -0
  26. data/lib/generators/ruact/scaffold/templates/request_spec.rb.tt +77 -0
  27. data/lib/generators/ruact/scaffold/templates/views/edit.html.erb.tt +7 -0
  28. data/lib/generators/ruact/scaffold/templates/views/index.html.erb.tt +6 -0
  29. data/lib/generators/ruact/scaffold/templates/views/new.html.erb.tt +5 -0
  30. data/lib/generators/ruact/scaffold/templates/views/show.html.erb.tt +16 -0
  31. data/lib/ruact/client_manifest.rb +37 -36
  32. data/lib/ruact/component_contract.rb +115 -0
  33. data/lib/ruact/configuration.rb +80 -28
  34. data/lib/ruact/controller.rb +79 -425
  35. data/lib/ruact/doctor.rb +133 -4
  36. data/lib/ruact/erb_preprocessor.rb +71 -12
  37. data/lib/ruact/erb_preprocessor_hook.rb +4 -1
  38. data/lib/ruact/errors.rb +30 -44
  39. data/lib/ruact/html_converter.rb +22 -1
  40. data/lib/ruact/manifest_resolver.rb +149 -0
  41. data/lib/ruact/railtie.rb +70 -203
  42. data/lib/ruact/render_pipeline.rb +8 -1
  43. data/lib/ruact/server.rb +28 -6
  44. data/lib/ruact/server_functions/codegen.rb +49 -188
  45. data/lib/ruact/server_functions/codegen_v2.rb +23 -6
  46. data/lib/ruact/server_functions/codegen_v2_query_params.rb +140 -0
  47. data/lib/ruact/server_functions/error_payload.rb +1 -1
  48. data/lib/ruact/server_functions/error_rendering.rb +22 -29
  49. data/lib/ruact/server_functions/name_bridge.rb +14 -16
  50. data/lib/ruact/server_functions/query_source.rb +35 -8
  51. data/lib/ruact/server_functions/route_source.rb +3 -4
  52. data/lib/ruact/server_functions/snapshot.rb +12 -139
  53. data/lib/ruact/server_functions/validation_errors.rb +70 -0
  54. data/lib/ruact/server_functions.rb +21 -25
  55. data/lib/ruact/signed_references.rb +162 -0
  56. data/lib/ruact/string_distance.rb +72 -0
  57. data/lib/ruact/validation_errors_collector.rb +139 -0
  58. data/lib/ruact/version.rb +1 -1
  59. data/lib/ruact/view_helper.rb +109 -0
  60. data/lib/ruact.rb +20 -19
  61. data/lib/tasks/ruact.rake +10 -53
  62. data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +3 -0
  63. data/spec/ruact/client_manifest_spec.rb +36 -0
  64. data/spec/ruact/component_contract_spec.rb +119 -0
  65. data/spec/ruact/configuration_spec.rb +51 -34
  66. data/spec/ruact/controller_request_spec.rb +264 -0
  67. data/spec/ruact/controller_spec.rb +70 -328
  68. data/spec/ruact/doctor_spec.rb +201 -0
  69. data/spec/ruact/erb_preprocessor_hook_spec.rb +4 -1
  70. data/spec/ruact/erb_preprocessor_spec.rb +127 -0
  71. data/spec/ruact/errors_spec.rb +0 -45
  72. data/spec/ruact/html_converter_spec.rb +50 -0
  73. data/spec/ruact/install_generator_spec.rb +591 -4
  74. data/spec/ruact/manifest_resolver_spec.rb +159 -0
  75. data/spec/ruact/query_request_spec.rb +109 -1
  76. data/spec/ruact/scaffold_generator_spec.rb +1835 -0
  77. data/spec/ruact/server_bucket_request_spec.rb +142 -0
  78. data/spec/ruact/server_function_name_spec.rb +1 -1
  79. data/spec/ruact/server_functions/codegen_spec.rb +158 -269
  80. data/spec/ruact/server_functions/name_bridge_spec.rb +9 -9
  81. data/spec/ruact/server_functions/query_source_spec.rb +51 -0
  82. data/spec/ruact/server_functions/railtie_integration_spec.rb +71 -268
  83. data/spec/ruact/server_functions/rake_spec.rb +29 -29
  84. data/spec/ruact/server_functions/snapshot_spec.rb +53 -213
  85. data/spec/ruact/server_rescue_request_spec.rb +6 -6
  86. data/spec/ruact/server_spec.rb +8 -9
  87. data/spec/ruact/signed_references_spec.rb +164 -0
  88. data/spec/ruact/string_distance_spec.rb +38 -0
  89. data/spec/ruact/validation_errors_spec.rb +116 -0
  90. data/spec/ruact/view_helper_spec.rb +79 -0
  91. data/spec/spec_helper.rb +15 -5
  92. data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +40 -45
  93. data/vendor/javascript/ruact-server-functions-runtime/index.js +42 -98
  94. data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +72 -75
  95. data/vendor/javascript/ruact-server-functions-runtime/package.json +1 -1
  96. data/vendor/javascript/vite-plugin-ruact/bootstrap.test.mjs +96 -0
  97. data/vendor/javascript/vite-plugin-ruact/index.js +372 -6
  98. data/vendor/javascript/vite-plugin-ruact/manifest-contract.test.mjs +182 -0
  99. data/vendor/javascript/vite-plugin-ruact/manifest-http.test.mjs +125 -0
  100. data/vendor/javascript/vite-plugin-ruact/package-lock.json +16 -0
  101. data/vendor/javascript/vite-plugin-ruact/package.json +3 -1
  102. data/vendor/javascript/vite-plugin-ruact/registry.test.mjs +199 -0
  103. data/vendor/javascript/vite-plugin-ruact/runtime/bootstrap.jsx +68 -0
  104. data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +235 -0
  105. data/vendor/javascript/vite-plugin-ruact/runtime/ruact-router.js +473 -0
  106. data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs +114 -139
  107. data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs +199 -262
  108. data/vendor/javascript/vite-plugin-ruact/tsconfig.json +18 -0
  109. data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold-agnostic.json +18 -0
  110. data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold.json +20 -0
  111. data/vendor/javascript/vite-plugin-ruact/type-tests/emitted-module.test-d.ts +23 -0
  112. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostDeleteDialog.tsx +90 -0
  113. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostForm.tsx +238 -0
  114. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostList.tsx +339 -0
  115. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostDeleteDialog.tsx +85 -0
  116. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostForm.tsx +216 -0
  117. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostList.tsx +269 -0
  118. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/ambient.d.ts +78 -0
  119. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/ambient.d.ts +166 -0
  120. data/vendor/javascript/vite-plugin-ruact/type-tests/typed-query.test-d.ts +48 -0
  121. data/vendor/javascript/vite-plugin-ruact/type-tests/usequery.test-d.ts +26 -0
  122. metadata +58 -15
  123. data/lib/generators/ruact/install/templates/application.jsx.tt +0 -51
  124. data/lib/ruact/server_action.rb +0 -131
  125. data/lib/ruact/server_functions/endpoint_controller.rb +0 -237
  126. data/lib/ruact/server_functions/registry.rb +0 -148
  127. data/lib/ruact/server_functions/registry_entry.rb +0 -26
  128. data/lib/ruact/server_functions/standalone_context.rb +0 -103
  129. data/lib/ruact/server_functions/standalone_dispatcher.rb +0 -178
  130. data/spec/ruact/server_functions/csrf_request_spec.rb +0 -380
  131. data/spec/ruact/server_functions/dispatch_request_spec.rb +0 -819
  132. data/spec/ruact/server_functions/registry_spec.rb +0 -199
  133. data/spec/ruact/server_functions/standalone_action_spec.rb +0 -224
  134. data/spec/ruact/server_functions/standalone_context_spec.rb +0 -142
  135. data/spec/ruact/server_functions/standalone_dispatcher_spec.rb +0 -273
@@ -23,10 +23,25 @@ require "rack/test"
23
23
 
24
24
  require "ruact/controller"
25
25
  require "ruact/server"
26
+ require "active_model"
26
27
 
27
28
  require_relative "controller_request_spec" unless defined?(ControllerRequestSpecSupport)
28
29
 
29
30
  module ServerBucketSpecSupport
31
+ # Story 13.3 (FR98) — an ActiveModel record with a presence validation, so a
32
+ # host action can exercise the non-exception `if record.valid? … else …`
33
+ # happy-failure path that `ruact_errors` round-trips.
34
+ class ValidatedPost
35
+ include ActiveModel::Model
36
+ include ActiveModel::Attributes
37
+
38
+ attribute :title, :string
39
+
40
+ validates :title, presence: true
41
+
42
+ def self.name = "ValidatedPost"
43
+ end
44
+
30
45
  # Serializable model — only :id/:title exposed; :secret must never leak.
31
46
  class BucketPost
32
47
  include Ruact::Serializable
@@ -96,6 +111,33 @@ module ServerBucketSpecSupport
96
111
  def unserializable
97
112
  @rec = UnserializableRecord.new
98
113
  end
114
+
115
+ # Story 13.3 (FR98) — opted-in FAILED validation: `ruact_errors(post)` after
116
+ # an invalid save attempt registers the canonical errors; Bucket-2 injects
117
+ # them under the reserved `errors` key alongside the serialized ivars.
118
+ def validated_create_invalid
119
+ post = ServerBucketSpecSupport::ValidatedPost.new(title: nil)
120
+ @created = post.valid? # false → populates post.errors
121
+ ruact_errors(post)
122
+ end
123
+
124
+ # Story 13.3 (FR98) — opted-in SUCCESS: the SAME call path yields
125
+ # `errors: {}` (success symmetry), so the client reads `result.errors`
126
+ # uniformly on both branches.
127
+ def validated_create_valid
128
+ post = ServerBucketSpecSupport::ValidatedPost.new(title: "Hi")
129
+ @created = post.valid? # true → post.errors empty
130
+ ruact_errors(post)
131
+ end
132
+
133
+ # Story 13.3 (FR98) — a stray dev ivar literally named `@errors` must NOT
134
+ # clobber the reserved round-trip key: the collector is the source of truth.
135
+ def stray_errors_ivar
136
+ @errors = "a dev string that must not win"
137
+ post = ServerBucketSpecSupport::ValidatedPost.new(title: nil)
138
+ post.valid?
139
+ ruact_errors(post)
140
+ end
99
141
  end
100
142
 
101
143
  # Review round 2 — an auth-style before_action that redirects on a Bucket-2
@@ -144,8 +186,31 @@ module ServerBucketSpecSupport
144
186
  render json: { token: form_authenticity_token }
145
187
  end
146
188
  end
189
+
190
+ # Story 10.0 (AC5) — the scaffold's resource-controller shape: includes BOTH
191
+ # Ruact::Controller and Ruact::Server (ApplicationController contributes
192
+ # Controller; the resource controller adds Server). An ivar-only GET page
193
+ # action (implicit `default_render`) backed by a conventional template must
194
+ # flow Server#default_render → super (GET is never a function call) → the
195
+ # now-fixed Controller#default_render → HTML shell, NOT a 500.
196
+ class ServerImplicitController < ActionController::Base
197
+ include Ruact::Controller
198
+ include Ruact::Server
199
+
200
+ def show; end
201
+ end
147
202
  end
148
203
 
204
+ # Story 10.0 (AC5) — conventional template under the shared app root so the
205
+ # Server-including controller's implicit GET reaches Controller#default_render.
206
+ ControllerRequestSpecSupport.write_view(
207
+ "server_bucket_spec_support/server_implicit", "show", <<~ERB
208
+ <div>
209
+ <DemoButton label={"hi"} />
210
+ </div>
211
+ ERB
212
+ )
213
+
149
214
  if defined?(ControllerRequestSpecSupport) &&
150
215
  !ControllerRequestSpecSupport.instance_variable_get(:@__ruact_server_bucket_routes_appended)
151
216
  ControllerRequestSpecSupport.instance_variable_set(:@__ruact_server_bucket_routes_appended, true)
@@ -159,9 +224,14 @@ if defined?(ControllerRequestSpecSupport) &&
159
224
  post "/bucket/vary_wildcard", to: "server_bucket_spec_support/bucket_server#vary_wildcard"
160
225
  post "/bucket/before_redirect", to: "server_bucket_spec_support/bucket_before_redirect#never_runs"
161
226
  post "/bucket/unserializable", to: "server_bucket_spec_support/bucket_server#unserializable"
227
+ post "/bucket/validated_create_invalid", to: "server_bucket_spec_support/bucket_server#validated_create_invalid"
228
+ post "/bucket/validated_create_valid", to: "server_bucket_spec_support/bucket_server#validated_create_valid"
229
+ post "/bucket/stray_errors_ivar", to: "server_bucket_spec_support/bucket_server#stray_errors_ivar"
162
230
  post "/bucket/dual_redirecting", to: "server_bucket_spec_support/bucket_dual#redirecting"
163
231
  post "/bucket/protected", to: "server_bucket_spec_support/bucket_forgery#create_protected"
164
232
  get "/bucket/csrf_token", to: "server_bucket_spec_support/bucket_forgery#csrf_token"
233
+ # Story 10.0 (AC5) — Server-including controller's implicit GET page action.
234
+ get "/server-implicit/show", to: "server_bucket_spec_support/server_implicit#show"
165
235
  end
166
236
  end
167
237
 
@@ -177,6 +247,10 @@ RSpec.describe "Story 9.2: Ruact::Server dual-bucket response negotiation", :sto
177
247
  before do
178
248
  Rails.logger = Logger.new(IO::NULL)
179
249
  ControllerRequestSpecSupport.boot!
250
+ # Story 10.0 (AC5) — pin Rails.root so the Server-path implicit-render
251
+ # template (`Rails.root/app/views`) is found regardless of full-suite
252
+ # ordering leftovers (see controller_request_spec.rb for the rationale).
253
+ Rails.root = ControllerRequestSpecSupport.app_root
180
254
  end
181
255
 
182
256
  def reset_config
@@ -349,4 +423,72 @@ RSpec.describe "Story 9.2: Ruact::Server dual-bucket response negotiation", :sto
349
423
  end
350
424
  end
351
425
  end
426
+
427
+ # Story 13.3 (FR98) — the Bucket-2 (imperative `await`) half of the
428
+ # Inertia-style validation `errors` round-trip (AC3).
429
+ describe "Story 13.3: Bucket-2 errors round-trip (FR98)", :story_13_3 do
430
+ it "FAILED validation injects the canonical errors under the reserved `errors` key" do
431
+ post "/bucket/validated_create_invalid", "{}", json_headers
432
+ expect(last_response.status).to eq(200)
433
+ body = JSON.parse(last_response.body)
434
+ expect(body).to eq("created" => false, "errors" => { "title" => ["Title can't be blank"] })
435
+ end
436
+
437
+ it "SUCCESS yields `errors: {}` through the same call path (success symmetry)" do
438
+ post "/bucket/validated_create_valid", "{}", json_headers
439
+ expect(last_response.status).to eq(200)
440
+ body = JSON.parse(last_response.body)
441
+ expect(body).to eq("created" => true, "errors" => {})
442
+ end
443
+
444
+ it "is set with Vary: Accept like every other non-GET shape" do
445
+ post "/bucket/validated_create_valid", "{}", json_headers
446
+ expect(last_response.headers["Vary"]).to match(/\bAccept\b/)
447
+ end
448
+
449
+ it "the collector wins over a stray dev ivar literally named @errors" do
450
+ post "/bucket/stray_errors_ivar", "{}", json_headers
451
+ expect(last_response.status).to eq(200)
452
+ body = JSON.parse(last_response.body)
453
+ expect(body.fetch("errors")).to eq("title" => ["Title can't be blank"])
454
+ end
455
+
456
+ it "preserves the 9.2 untouched-collector contract: an empty opted-out action still 204s" do
457
+ # `empty_action` never calls `ruact_errors` → no `errors` key, 204 No Content.
458
+ post "/bucket/empty", "{}", json_headers
459
+ expect(last_response.status).to eq(204)
460
+ expect(last_response.body).to eq("")
461
+ end
462
+
463
+ it "preserves the 9.2 untouched-collector contract: a populated action carries NO errors key" do
464
+ # `with_ivars` sets ivars but never opts in → body has no `errors` key.
465
+ post "/bucket/with_ivars", "{}", json_headers
466
+ expect(JSON.parse(last_response.body)).not_to have_key("errors")
467
+ end
468
+
469
+ # AC5(e) coexistence: the RAISED `ActiveRecord::RecordInvalid` path still
470
+ # renders the structured 422 error payload — proven (unchanged) by
471
+ # server_rescue_request_spec.rb's `record_invalid` example. 13.3 is the
472
+ # NON-raised 200 path above; the two contracts stay distinct.
473
+ end
474
+
475
+ # Story 10.0 (AC5) — the Server-path inherits the default_render graceful
476
+ # degradation fix. A GET is never a function call, so Server#default_render
477
+ # delegates to super → the fixed Controller#default_render.
478
+ describe "Story 10.0: Server path inherits non-HTML Accept graceful degradation (AC5)", :story_10_0 do
479
+ it "GET with Accept: */* returns the HTML shell, not a 500" do
480
+ get "/server-implicit/show", {}, { "HTTP_ACCEPT" => "*/*" }
481
+ expect(last_response.status).to(eq(200),
482
+ "expected 200, got #{last_response.status} body=#{last_response.body[0, 400]}")
483
+ expect(last_response.headers["Content-Type"]).to include("text/html")
484
+ expect(last_response.body).to include("DemoButton")
485
+ expect(last_response.body).not_to include("__ruact_component__ called outside a ruact_render flow")
486
+ end
487
+
488
+ it "GET with Accept: text/x-component returns a Flight payload (function-call path untouched)" do
489
+ get "/server-implicit/show", {}, { "HTTP_ACCEPT" => "text/x-component" }
490
+ expect(last_response.status).to eq(200)
491
+ expect(last_response.headers["Content-Type"]).to include("text/x-component")
492
+ end
493
+ end
352
494
  end
@@ -39,7 +39,7 @@ RSpec.describe "Ruact::Server.ruact_function_name", :story_9_3 do
39
39
  end.to raise_error(Ruact::ConfigurationError, /reserved/)
40
40
  end
41
41
 
42
- it "rejects a runtime-bound name (revalidate / _makeRef)" do
42
+ it "rejects a runtime-bound name (revalidate)" do
43
43
  expect do
44
44
  host.ruact_function_name(:create, as: "revalidate")
45
45
  end.to raise_error(Ruact::ConfigurationError, /reserved|ruact runtime/)