ruact 0.0.10 → 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 +123 -48
  3. data/CONTRIBUTING.md +233 -0
  4. data/README.md +16 -6
  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 -1
  24. data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -92
  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,202 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "active_support/all"
5
- require "action_dispatch"
6
- require "ruact/server_functions/route_source"
7
-
8
- module Ruact
9
- module ServerFunctions
10
- # Story 9.3 — route introspection: Rails.application.routes (filtered to
11
- # non-GET routes on controllers that include Ruact::Server) → v2 mutation
12
- # entries with the derivation table locked in the 2026-06-09 ADR addendum.
13
- RSpec.describe RouteSource, :story_9_3 do
14
- # Builds an isolated RouteSet so specs never depend on a host app's routes.
15
- def route_set(&blk)
16
- rs = ActionDispatch::Routing::RouteSet.new
17
- rs.draw(&blk)
18
- rs
19
- end
20
-
21
- # All controllers count as Ruact::Server hosts (isolates the derivation
22
- # table from constant-resolution); the filter behaviour is tested
23
- # separately below.
24
- def collect(routes, overrides: {})
25
- described_class.collect(
26
- routes,
27
- host_predicate: ->(_controller) { true },
28
- overrides_for: ->(controller) { overrides.fetch(controller, {}) }
29
- )
30
- end
31
-
32
- def ids(entries) = entries.map { |e| e["js_identifier"] }
33
-
34
- describe "derivation table (AC3)" do
35
- it "names the RESTful collection create as singular verb+resource" do
36
- rs = route_set { resources :posts, only: %i[create] }
37
- entry = collect(rs).fetch(0)
38
- expect(entry["js_identifier"]).to eq("createPost")
39
- expect(entry["http_method"]).to eq("POST")
40
- expect(entry["path"]).to eq("/posts")
41
- expect(entry["segments"]).to eq([])
42
- expect(entry["kind"]).to eq("action")
43
- end
44
-
45
- it "names update (member) singular and prefers PATCH over the PUT alias" do
46
- rs = route_set { resources :posts, only: %i[update] }
47
- entries = collect(rs)
48
- expect(entries.size).to eq(1) # PATCH + PUT collapse to one entry
49
- entry = entries.fetch(0)
50
- expect(entry["js_identifier"]).to eq("updatePost")
51
- expect(entry["http_method"]).to eq("PATCH")
52
- expect(entry["path"]).to eq("/posts/:id")
53
- expect(entry["segments"]).to eq(["id"])
54
- end
55
-
56
- it "names destroy (member) singular" do
57
- rs = route_set { resources :posts, only: %i[destroy] }
58
- entry = collect(rs).fetch(0)
59
- expect(entry["js_identifier"]).to eq("destroyPost")
60
- expect(entry["http_method"]).to eq("DELETE")
61
- expect(entry["path"]).to eq("/posts/:id")
62
- end
63
-
64
- it "skips the GET RESTful actions (index/show/new/edit)" do
65
- rs = route_set { resources :posts }
66
- expect(ids(collect(rs))).to match_array(%w[createPost updatePost destroyPost])
67
- end
68
-
69
- it "names a custom member route singular (verb-action + singular resource)" do
70
- rs = route_set do
71
- resources :posts do
72
- member { post :publish }
73
- end
74
- end
75
- entry = collect(rs).find { |e| e["js_identifier"] == "publishPost" }
76
- expect(entry).not_to be_nil
77
- expect(entry["http_method"]).to eq("POST")
78
- expect(entry["path"]).to eq("/posts/:id/publish")
79
- expect(entry["segments"]).to eq(["id"])
80
- end
81
-
82
- it "names a custom collection route plural" do
83
- rs = route_set do
84
- resources :posts do
85
- collection { post :publish_all }
86
- end
87
- end
88
- entry = collect(rs).find { |e| e["js_identifier"] == "publishAllPosts" }
89
- expect(entry).not_to be_nil
90
- expect(entry["path"]).to eq("/posts/publish_all")
91
- expect(entry["segments"]).to eq([])
92
- end
93
-
94
- it "names singular-resource create/destroy singular" do
95
- rs = route_set { resource :session, only: %i[create destroy] }
96
- expect(ids(collect(rs))).to match_array(%w[createSession destroySession])
97
- end
98
-
99
- it "prefixes namespaced controllers (verb + Namespace + Resource)" do
100
- rs = route_set { namespace(:admin) { resources :posts, only: %i[create update] } }
101
- expect(ids(collect(rs))).to match_array(%w[createAdminPost updateAdminPost])
102
- end
103
-
104
- it "deep-prefixes multi-level namespaces" do
105
- rs = route_set do
106
- namespace(:admin) { namespace(:reports) { resources :posts, only: %i[create] } }
107
- end
108
- expect(ids(collect(rs))).to eq(%w[createAdminReportsPost])
109
- end
110
-
111
- it "sorts entries by js_identifier for deterministic output" do
112
- rs = route_set { resources :posts }
113
- expect(ids(collect(rs))).to eq(%w[createPost destroyPost updatePost])
114
- end
115
-
116
- it "classifies a custom-param member route (param: :slug) as member → singular" do
117
- rs = route_set do
118
- resources :posts, param: :slug do
119
- member { post :publish }
120
- end
121
- end
122
- entry = collect(rs).find { |e| e["action"] == "publish" }
123
- expect(entry["js_identifier"]).to eq("publishPost")
124
- expect(entry["segments"]).to eq(["slug"])
125
- end
126
-
127
- it "classifies a nested collection route (only parent :id present) as collection → plural" do
128
- rs = route_set do
129
- resources :posts, only: [] do
130
- resources :comments, only: [] do
131
- collection { post :flag_all }
132
- end
133
- end
134
- end
135
- entry = collect(rs).find { |e| e["action"] == "flag_all" }
136
- expect(entry["js_identifier"]).to eq("flagAllComments")
137
- end
138
- end
139
-
140
- describe "rename override (AC4 input)" do
141
- it "uses the per-controller override identifier when present" do
142
- rs = route_set do
143
- resources :posts do
144
- collection { post :publish_all }
145
- end
146
- end
147
- entries = collect(rs, overrides: { "posts" => { "publish_all" => "publishEverything" } })
148
- expect(ids(entries)).to include("publishEverything")
149
- expect(ids(entries)).not_to include("publishAllPosts")
150
- end
151
- end
152
-
153
- describe "collision detection (AC4)" do
154
- it "fails loudly naming BOTH origins when two routes map to the same identifier" do
155
- rs = route_set do
156
- resources :posts, only: %i[create]
157
- resources :comments, only: %i[create]
158
- end
159
- # Force a collision: comments#create is renamed onto posts#create's name.
160
- overrides = { "comments" => { "create" => "createPost" } }
161
- expect do
162
- collect(rs, overrides: overrides)
163
- end.to raise_error(Ruact::ConfigurationError, /naming collision/) { |err|
164
- expect(err.message).to include("posts#create")
165
- expect(err.message).to include("comments#create")
166
- expect(err.message).to include("createPost")
167
- }
168
- end
169
-
170
- it "boot succeeds once the colliding route is renamed to a free identifier (full cycle)" do
171
- rs = route_set do
172
- resources :posts, only: %i[create]
173
- resources :comments, only: %i[create]
174
- end
175
- overrides = { "comments" => { "create" => "createComment" } }
176
- ids_out = ids(collect(rs, overrides: overrides))
177
- expect(ids_out).to eq(%w[createComment createPost])
178
- end
179
- end
180
-
181
- describe "host filter (Ruact::Server only)" do
182
- it "skips routes whose controller is not a Ruact::Server host" do
183
- rs = route_set do
184
- resources :posts, only: %i[create]
185
- resources :widgets, only: %i[create]
186
- end
187
- entries = described_class.collect(
188
- rs,
189
- host_predicate: ->(controller) { controller == "posts" },
190
- overrides_for: ->(_c) { {} }
191
- )
192
- expect(ids(entries)).to eq(%w[createPost])
193
- end
194
-
195
- it "skips GET/HEAD routes even on a host controller" do
196
- rs = route_set { get "/posts/search", to: "posts#search" }
197
- expect(collect(rs)).to be_empty
198
- end
199
- end
200
- end
201
- end
202
- end
@@ -1,96 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "tmpdir"
5
-
6
- module Ruact
7
- module ServerFunctions
8
- RSpec.describe Snapshot, :story_9_3 do
9
- let(:frozen_time) { Time.utc(2026, 5, 13, 12, 34, 56) }
10
- let(:entries) do
11
- [
12
- { "js_identifier" => "createPost", "kind" => "action", "http_method" => "POST",
13
- "path" => "/posts", "segments" => [], "controller" => "posts", "action" => "create" }
14
- ]
15
- end
16
-
17
- describe ".dump_v2" do
18
- it "wraps entries in a version-2 snapshot Hash" do
19
- snap = described_class.dump_v2(entries, now: frozen_time)
20
- expect(snap[:version]).to eq(2)
21
- expect(snap[:generated_at]).to eq(frozen_time.iso8601)
22
- expect(snap[:functions]).to eq(entries)
23
- end
24
- end
25
-
26
- describe ".generate_v2! (write-if-changed)" do
27
- around do |example|
28
- Dir.mktmpdir do |dir|
29
- @tmpdir = dir
30
- example.run
31
- end
32
- end
33
-
34
- let(:path) { File.join(@tmpdir, "server-functions.json") }
35
-
36
- it "writes a version-2 bridge on first call" do
37
- expect(described_class.generate_v2!(entries: entries, path: path, now: frozen_time)).to be(true)
38
- parsed = JSON.parse(File.read(path))
39
- expect(parsed.fetch("version")).to eq(2)
40
- expect(parsed.fetch("functions").first.fetch("js_identifier")).to eq("createPost")
41
- end
42
-
43
- it "short-circuits when entries are unchanged (no timestamp churn)" do
44
- described_class.generate_v2!(entries: entries, path: path, now: frozen_time)
45
- expect(described_class.generate_v2!(entries: entries, path: path, now: Time.now.utc)).to be(false)
46
- end
47
-
48
- it "rewrites when entries change" do
49
- described_class.generate_v2!(entries: entries, path: path, now: frozen_time)
50
- more = entries + [{ "js_identifier" => "destroyPost", "kind" => "action",
51
- "http_method" => "DELETE", "path" => "/posts/:id",
52
- "segments" => ["id"], "controller" => "posts", "action" => "destroy" }]
53
- expect(described_class.generate_v2!(entries: more, path: path, now: frozen_time)).to be(true)
54
- end
55
-
56
- it "creates the parent directory if missing" do
57
- nested = File.join(@tmpdir, "deep", "nested", "server-functions.json")
58
- expect(described_class.generate_v2!(entries: entries, path: nested, now: frozen_time)).to be(true)
59
- expect(File.exist?(nested)).to be(true)
60
- end
61
-
62
- it "recovers from a corrupted existing file by overwriting it" do
63
- File.write(path, "{ not json")
64
- expect(described_class.generate_v2!(entries: entries, path: path, now: frozen_time)).to be(true)
65
- expect(JSON.parse(File.read(path)).fetch("version")).to eq(2)
66
- end
67
-
68
- # Story 13.4 — the new typed-query metadata must survive the JSON
69
- # round-trip unchanged AND keep the file byte-stable (no churn on a
70
- # second pass with identical entries).
71
- context "with typed-query params metadata (Story 13.4)", :story_13_4 do
72
- let(:query_entries) do
73
- [
74
- { "js_identifier" => "searchUsers", "kind" => "query", "http_method" => "GET",
75
- "path" => "/q/searchUsers", "segments" => [], "accepts_params" => true,
76
- "params" => [{ "name" => "term", "required" => true },
77
- { "name" => "limit", "required" => false }],
78
- "params_rest" => false, "controller" => "CatalogQuery", "action" => "search_users" }
79
- ]
80
- end
81
-
82
- it "round-trips the params metadata unchanged" do
83
- described_class.generate_v2!(entries: query_entries, path: path, now: frozen_time)
84
- parsed = JSON.parse(File.read(path))
85
- expect(parsed.fetch("functions")).to eq(query_entries)
86
- end
87
-
88
- it "is byte-stable: a second pass with identical entries writes nothing" do
89
- described_class.generate_v2!(entries: query_entries, path: path, now: frozen_time)
90
- expect(described_class.generate_v2!(entries: query_entries, path: path, now: Time.now.utc)).to be(false)
91
- end
92
- end
93
- end
94
- end
95
- end
96
- end
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "tmpdir"
5
-
6
- module Ruact
7
- module ServerFunctions
8
- RSpec.describe SnapshotWriter, :story_8_0a do
9
- around do |example|
10
- Dir.mktmpdir do |dir|
11
- @tmpdir = dir
12
- example.run
13
- end
14
- end
15
-
16
- let(:path) { File.join(@tmpdir, "out.txt") }
17
-
18
- describe ".write_if_changed! (Story 8.0a — atomic, byte-aware writer)" do
19
- it "writes the file when it does not yet exist and returns true" do
20
- expect(described_class.write_if_changed!(path: path, content: "hello\n"))
21
- .to be(true)
22
- expect(File.read(path)).to eq("hello\n")
23
- end
24
-
25
- it "skips writing when the existing content matches byte-for-byte" do
26
- File.write(path, "hello\n")
27
- original_mtime = File.mtime(path)
28
- sleep 1.05
29
-
30
- expect(described_class.write_if_changed!(path: path, content: "hello\n"))
31
- .to be(false)
32
- expect(File.mtime(path)).to eq(original_mtime)
33
- end
34
-
35
- it "writes when the existing content differs by even a single byte" do
36
- File.write(path, "hello\n")
37
- expect(described_class.write_if_changed!(path: path, content: "hello!\n"))
38
- .to be(true)
39
- expect(File.read(path)).to eq("hello!\n")
40
- end
41
-
42
- it "creates missing parent directories" do
43
- nested = File.join(@tmpdir, "a", "b", "c", "out.txt")
44
- described_class.write_if_changed!(path: nested, content: "x")
45
- expect(File.read(nested)).to eq("x")
46
- end
47
-
48
- it "writes via a same-directory tmpfile so partial reads never see " \
49
- "a torn file (Story 8.0a)" do
50
- described_class.write_if_changed!(path: path, content: "atomic\n")
51
- # After the write the temp sibling must not linger.
52
- siblings = Dir.children(@tmpdir)
53
- expect(siblings).to eq(["out.txt"])
54
- end
55
-
56
- it "raises Ruact::ConfigurationError when the parent directory is unwritable " \
57
- "(Story 8.0a)" do
58
- read_only = File.join(@tmpdir, "ro")
59
- FileUtils.mkdir_p(read_only)
60
- FileUtils.chmod(0o500, read_only)
61
- target = File.join(read_only, "nested", "out.txt")
62
-
63
- expect { described_class.write_if_changed!(path: target, content: "x") }
64
- .to raise_error(Ruact::ConfigurationError, /cannot create/)
65
- ensure
66
- FileUtils.chmod(0o700, read_only) if read_only && File.exist?(read_only)
67
- end
68
- end
69
- end
70
- end
71
- end