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,193 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "ruact/server_functions"
5
- require "ruact/server_functions/query_source"
6
-
7
- module Ruact
8
- module ServerFunctions
9
- # Plain query classes — only `instance_method(...).parameters` (kwargs
10
- # presence) and `.name` (collision origin) are read. Defined at module
11
- # level so they are not example-group-leaky constants.
12
- class CatalogQ
13
- def categories; end
14
-
15
- def search_users(term:, limit: 10); end
16
- end
17
-
18
- class PeopleQ
19
- def search_users(term:); end
20
- end
21
-
22
- # Story 13.4 — kwarg shapes exercising the per-param metadata derivation:
23
- # a `**keyrest` (anonymous + named), and a positional that must be ignored.
24
- class ParamShapesQ
25
- def with_rest(scope:, **opts); end
26
-
27
- def rest_only(**opts); end
28
-
29
- def positional_ignored(id, query:); end
30
- end
31
-
32
- # Story 9.5 — query introspection: the drawn route table (filtered to the
33
- # generated query dispatch controllers) → v2 query entries. Pure: route set
34
- # and the query-class resolver are injected, so the derivation is testable
35
- # with no Rails boot and no controllers (a fake route set + plain query
36
- # classes).
37
- RSpec.describe QuerySource, :story_9_5 do
38
- # Minimal route double exposing the surface QuerySource reads:
39
- # `defaults[:controller]`, `defaults[:action]`, `path.spec.to_s`.
40
- def query_route(controller, action, path)
41
- spec = Object.new
42
- spec.define_singleton_method(:to_s) { "#{path}(.:format)" }
43
- path_obj = Object.new
44
- path_obj.define_singleton_method(:spec) { spec }
45
- route = Object.new
46
- route.define_singleton_method(:defaults) { { controller: controller, action: action } }
47
- route.define_singleton_method(:path) { path_obj }
48
- route
49
- end
50
-
51
- def route_set(routes)
52
- rs = Object.new
53
- rs.define_singleton_method(:routes) { routes }
54
- rs
55
- end
56
-
57
- let(:prefix) { QuerySource::QUERY_CONTROLLER_PREFIX }
58
-
59
- def resolver(map)
60
- ->(controller) { map[controller] }
61
- end
62
-
63
- describe ".collect" do
64
- it "emits one query entry per mounted query route (route-truth)" do
65
- rs = route_set([
66
- query_route("#{prefix}catalog_q", "categories", "/q/categories"),
67
- query_route("#{prefix}catalog_q", "search_users", "/q/searchUsers")
68
- ])
69
- entries = described_class.collect(rs, query_class_for: resolver("#{prefix}catalog_q" => CatalogQ))
70
-
71
- expect(entries.map { |e| e["js_identifier"] }).to eq(%w[categories searchUsers])
72
- expect(entries).to all(include("kind" => "query", "http_method" => "GET", "segments" => []))
73
- end
74
-
75
- it "derives jsId via NameBridge and carries the clean path" do
76
- rs = route_set([query_route("#{prefix}catalog_q", "search_users", "/q/searchUsers")])
77
- entry = described_class.collect(rs, query_class_for: resolver("#{prefix}catalog_q" => CatalogQ)).first
78
- expect(entry["js_identifier"]).to eq("searchUsers")
79
- expect(entry["path"]).to eq("/q/searchUsers")
80
- expect(entry["controller"]).to eq("Ruact::ServerFunctions::CatalogQ")
81
- expect(entry["action"]).to eq("search_users")
82
- end
83
-
84
- it "flags accepts_params true when the method declares kwargs, false otherwise" do
85
- rs = route_set([
86
- query_route("#{prefix}catalog_q", "categories", "/q/categories"),
87
- query_route("#{prefix}catalog_q", "search_users", "/q/searchUsers")
88
- ])
89
- entries = described_class.collect(rs, query_class_for: resolver("#{prefix}catalog_q" => CatalogQ))
90
- by_id = entries.to_h { |e| [e["js_identifier"], e] }
91
- expect(by_id["categories"]["accepts_params"]).to be(false)
92
- expect(by_id["searchUsers"]["accepts_params"]).to be(true)
93
- end
94
-
95
- it "derives per-kwarg params metadata (name + required/optional, declaration order)", :story_13_4 do
96
- rs = route_set([query_route("#{prefix}catalog_q", "search_users", "/q/searchUsers")])
97
- entry = described_class.collect(rs, query_class_for: resolver("#{prefix}catalog_q" => CatalogQ)).first
98
- expect(entry["params"]).to eq([
99
- { "name" => "term", "required" => true },
100
- { "name" => "limit", "required" => false }
101
- ])
102
- expect(entry["params_rest"]).to be(false)
103
- end
104
-
105
- it "emits empty params + accepts_params false for a no-kwargs query", :story_13_4 do
106
- rs = route_set([query_route("#{prefix}catalog_q", "categories", "/q/categories")])
107
- entry = described_class.collect(rs, query_class_for: resolver("#{prefix}catalog_q" => CatalogQ)).first
108
- expect(entry["params"]).to eq([])
109
- expect(entry["params_rest"]).to be(false)
110
- expect(entry["accepts_params"]).to be(false)
111
- end
112
-
113
- it "flags params_rest for a `**keyrest` and still carries the named keys", :story_13_4 do
114
- rs = route_set([query_route("#{prefix}params_q", "with_rest", "/q/withRest")])
115
- entry = described_class.collect(rs, query_class_for: resolver("#{prefix}params_q" => ParamShapesQ)).first
116
- expect(entry["params"]).to eq([{ "name" => "scope", "required" => true }])
117
- expect(entry["params_rest"]).to be(true)
118
- expect(entry["accepts_params"]).to be(true)
119
- end
120
-
121
- it "treats a `**keyrest`-only query as accepts_params (rest, no named keys)", :story_13_4 do
122
- rs = route_set([query_route("#{prefix}params_q", "rest_only", "/q/restOnly")])
123
- entry = described_class.collect(rs, query_class_for: resolver("#{prefix}params_q" => ParamShapesQ)).first
124
- expect(entry["params"]).to eq([])
125
- expect(entry["params_rest"]).to be(true)
126
- expect(entry["accepts_params"]).to be(true)
127
- end
128
-
129
- it "ignores positional params (only kwargs are FR88 query params)", :story_13_4 do
130
- rs = route_set([query_route("#{prefix}params_q", "positional_ignored", "/q/positionalIgnored")])
131
- entry = described_class.collect(rs, query_class_for: resolver("#{prefix}params_q" => ParamShapesQ)).first
132
- expect(entry["params"]).to eq([{ "name" => "query", "required" => true }])
133
- expect(entry["params_rest"]).to be(false)
134
- end
135
-
136
- it "ignores non-query routes (controller not under the dispatch namespace)" do
137
- rs = route_set([
138
- query_route("posts", "create", "/posts"),
139
- query_route("#{prefix}catalog_q", "categories", "/q/categories")
140
- ])
141
- entries = described_class.collect(rs, query_class_for: resolver("#{prefix}catalog_q" => CatalogQ))
142
- expect(entries.map { |e| e["js_identifier"] }).to eq(%w[categories])
143
- end
144
-
145
- it "skips a query route whose class cannot be resolved (nil resolver result)" do
146
- rs = route_set([query_route("#{prefix}gone_q", "categories", "/q/categories")])
147
- expect(described_class.collect(rs, query_class_for: resolver({}))).to eq([])
148
- end
149
-
150
- it "raises a query×query collision naming both origins" do
151
- rs = route_set([
152
- query_route("#{prefix}catalog_q", "search_users", "/q/searchUsers"),
153
- query_route("#{prefix}people_q", "search_users", "/q/searchUsers")
154
- ])
155
- map = { "#{prefix}catalog_q" => CatalogQ, "#{prefix}people_q" => PeopleQ }
156
- expect { described_class.collect(rs, query_class_for: resolver(map)) }
157
- .to raise_error(Ruact::ConfigurationError, /CatalogQ#search_users and .*PeopleQ#search_users/m)
158
- end
159
- end
160
- end
161
-
162
- # Story 9.5 (Task 2) — the merged JS namespace (route entries + query
163
- # entries share it). The route×query side is detected at the codegen
164
- # combine point.
165
- RSpec.describe ".detect_merged_namespace_collisions!", :story_9_5 do
166
- def action_entry(js_id, controller, action)
167
- { "js_identifier" => js_id, "kind" => "action", "controller" => controller, "action" => action }
168
- end
169
-
170
- def query_entry(js_id, controller, action)
171
- { "js_identifier" => js_id, "kind" => "query", "controller" => controller, "action" => action }
172
- end
173
-
174
- it "raises on a route×query collision naming both origins + the rename escape hatch" do
175
- entries = [
176
- action_entry("categories", "posts", "categories"),
177
- query_entry("categories", "CatalogQuery", "categories")
178
- ]
179
- expect { ServerFunctions.detect_merged_namespace_collisions!(entries) }
180
- .to raise_error(Ruact::ConfigurationError,
181
- /posts#categories and CatalogQuery#categories.*ruact_function_name/m)
182
- end
183
-
184
- it "does not raise when the rename makes the identifiers distinct" do
185
- entries = [
186
- action_entry("listCategories", "posts", "categories"), # renamed via ruact_function_name
187
- query_entry("categories", "CatalogQuery", "categories")
188
- ]
189
- expect { ServerFunctions.detect_merged_namespace_collisions!(entries) }.not_to raise_error
190
- end
191
- end
192
- end
193
- end
@@ -1,215 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "tmpdir"
5
- require "fileutils"
6
- require "action_controller"
7
-
8
- # Spec_helper's rails_stub defines Rails; the railtie was not auto-required
9
- # because Rails was not yet defined when ruact.rb evaluated `require_relative
10
- # "ruact/railtie" if defined?(Rails)`. Load it explicitly (mirrors
11
- # spec/ruact/railtie_spec.rb).
12
- require "ruact/railtie"
13
- require "ruact/controller"
14
- require "ruact/server"
15
- require "ruact/routing"
16
- require "ruact/query"
17
-
18
- # Story 9.9 — the route-driven (v2) codegen is the sole writer of the real
19
- # bridge. `Railtie.write_server_functions_snapshot!` is the entry point wired
20
- # into `config.to_prepare`; it forces the route table to load, then delegates to
21
- # `ServerFunctions.write_v2_snapshot!`, which writes the v2 snapshot to the REAL
22
- # bridge (`tmp/cache/ruact/server-functions.json`).
23
- module Ruact
24
- module ServerFunctions
25
- RSpec.describe "Ruact::Railtie.write_server_functions_snapshot! (Story 9.9)", :story_9_9 do
26
- before do
27
- stub_const("WrapperDemoPostsController", Class.new(ActionController::Base) { include Ruact::Server })
28
- end
29
-
30
- around do |example|
31
- Dir.mktmpdir do |dir|
32
- original_root = Rails.root
33
- Rails.root = Pathname.new(dir)
34
- @tmpdir = dir
35
- example.run
36
- ensure
37
- Rails.root = original_root
38
- end
39
- end
40
-
41
- let(:path) { File.join(@tmpdir, "tmp/cache/ruact/server-functions.json") }
42
-
43
- # Drives the wrapper by stubbing Rails.application with a routes set + a
44
- # no-op routes_reloader (the cold-boot ordering fold-in).
45
- def stub_application(routes)
46
- reloader = Object.new
47
- def reloader.execute_unless_loaded; end
48
- app = Object.new
49
- app.define_singleton_method(:routes) { routes }
50
- app.define_singleton_method(:routes_reloader) { reloader }
51
- allow(Rails).to receive(:application).and_return(app)
52
- end
53
-
54
- def route_set
55
- rs = ActionDispatch::Routing::RouteSet.new
56
- rs.draw { resources :wrapper_demo_posts, only: %i[create update destroy] }
57
- rs
58
- end
59
-
60
- it "writes the v2 JSON to the REAL bridge (tmp/cache/ruact/server-functions.json)" do
61
- stub_application(route_set)
62
- entries = Ruact::Railtie.write_server_functions_snapshot!
63
- expect(entries.map { |e| e["js_identifier"] })
64
- .to match_array(%w[createWrapperDemoPost updateWrapperDemoPost destroyWrapperDemoPost])
65
- expect(File).to exist(path)
66
- expect(JSON.parse(File.read(path)).fetch("version")).to eq(2)
67
- end
68
-
69
- it "writes an empty `functions: []` array when no routes are exposed" do
70
- empty = ActionDispatch::Routing::RouteSet.new
71
- empty.draw { get "/health", to: "health#show" }
72
- stub_application(empty)
73
- Ruact::Railtie.write_server_functions_snapshot!
74
- expect(JSON.parse(File.read(path)).fetch("functions")).to eq([])
75
- end
76
-
77
- it "forces the route table to load before reading it (cold-boot ordering fold-in)" do
78
- reloader = Object.new
79
- called = false
80
- reloader.define_singleton_method(:execute_unless_loaded) { called = true }
81
- routes = route_set
82
- app = Object.new
83
- app.define_singleton_method(:routes) { routes }
84
- app.define_singleton_method(:routes_reloader) { reloader }
85
- allow(Rails).to receive(:application).and_return(app)
86
- Ruact::Railtie.write_server_functions_snapshot!
87
- expect(called).to be(true)
88
- end
89
- end
90
-
91
- RSpec.describe "Ruact::ServerFunctions.write_v2_snapshot! (Story 9.3)", :story_9_3 do
92
- # A real Ruact::Server host so RouteSource's default constant-resolving
93
- # host predicate recognizes it; stub_const (not a literal class) keeps the
94
- # file single-definition and the constant scoped to the example.
95
- before do
96
- stub_const("V2DemoPostsController", Class.new(ActionController::Base) { include Ruact::Server })
97
- end
98
-
99
- around do |example|
100
- Dir.mktmpdir { |dir| @tmpdir = dir and example.run }
101
- end
102
-
103
- def route_set
104
- rs = ActionDispatch::Routing::RouteSet.new
105
- rs.draw { resources :v2_demo_posts, only: %i[create update destroy] }
106
- rs
107
- end
108
-
109
- def write!(logger: nil)
110
- Ruact::ServerFunctions.write_v2_snapshot!(
111
- route_set: route_set, root: Pathname.new(@tmpdir), logger: logger
112
- )
113
- end
114
-
115
- # Story 9.9 — the v2 codegen writes the REAL bridge (the `.next` parallel
116
- # target was demolished).
117
- let(:real_json) { File.join(@tmpdir, "tmp/cache/ruact/server-functions.json") }
118
- let(:real_ts) { File.join(@tmpdir, "app/javascript/.ruact/server-functions.ts") }
119
-
120
- it "writes the v2 bridge + TS to the REAL target" do
121
- entries = write!
122
-
123
- expect(entries.map { |e| e["js_identifier"] })
124
- .to match_array(%w[createV2DemoPost updateV2DemoPost destroyV2DemoPost])
125
- expect(File).to exist(real_json)
126
- expect(File).to exist(real_ts)
127
- expect(JSON.parse(File.read(real_json)).fetch("version")).to eq(2)
128
- end
129
-
130
- it "renders _makeServerFunction calls targeting real routes into the TS" do
131
- write!
132
- ts = File.read(real_ts)
133
- expect(ts).to include('import { _makeServerFunction } from "ruact/server-functions-runtime";')
134
- expect(ts).to include('_makeServerFunction({ method: "POST", path: "/v2_demo_posts", segments: [] });')
135
- expect(ts).to include('_makeServerFunction({ method: "PATCH", path: "/v2_demo_posts/:id", segments: ["id"] });')
136
- end
137
-
138
- it "is byte-stable across calls on an unchanged route table (no churn)" do
139
- write!
140
- first = File.read(real_ts)
141
- write!
142
- expect(File.read(real_ts)).to eq(first)
143
- end
144
-
145
- it "logs the exposed function names (AC2 — transparency over silence)" do
146
- logger = instance_double(Logger, info: nil)
147
- write!(logger: logger)
148
- expect(logger).to have_received(:info).with(/\[ruact\] codegen: exposing .*createV2DemoPost/)
149
- end
150
- end
151
-
152
- RSpec.describe "Ruact::ServerFunctions.write_v2_snapshot! — queries (Story 9.5)", :story_9_5 do
153
- around do |example|
154
- Dir.mktmpdir { |dir| @tmpdir = dir and example.run }
155
- end
156
-
157
- before do
158
- stub_const("V2QueryParentController", Class.new(ActionController::Base))
159
- Ruact.configure { |c| c.query_parent_controller = "V2QueryParentController" }
160
- stub_const("V2CatalogQuery", Class.new(Ruact::Query) do
161
- def categories; end
162
- def search(term:); end
163
- end)
164
- end
165
-
166
- def route_set
167
- route_set = ActionDispatch::Routing::RouteSet.new
168
- route_set.draw { ruact_queries V2CatalogQuery }
169
- route_set
170
- end
171
-
172
- def write!(routes = route_set)
173
- Ruact::ServerFunctions.write_v2_snapshot!(route_set: routes, root: Pathname.new(@tmpdir))
174
- end
175
-
176
- let(:real_ts) { File.join(@tmpdir, "app/javascript/.ruact/server-functions.ts") }
177
-
178
- it "emits query entries (route-truth) merged into the v2 snapshot" do
179
- entries = write!
180
- expect(entries.map { |e| e["js_identifier"] }).to match_array(%w[categories search])
181
- expect(entries).to all(include("kind" => "query", "http_method" => "GET"))
182
- end
183
-
184
- it "renders _makeQuery refs + the useQuery re-export into the TS" do
185
- write!
186
- ts = File.read(real_ts)
187
- expect(ts).to include('import { _makeQuery } from "ruact/server-functions-runtime";')
188
- expect(ts).to include('_makeQuery({ path: "/q/categories", kind: "query" });')
189
- expect(ts).to include("export const categories: () => Promise<unknown> =")
190
- # Story 13.4 — the real pipeline now narrows `params` to the declared
191
- # kwargs end-to-end (`def search(term:)` → a required `term` of the FR88
192
- # wire union), not the opaque `Record<string, unknown>`.
193
- expect(ts).to include(
194
- "export const search: (params: { term: string | number | boolean | null }) => Promise<unknown> ="
195
- )
196
- expect(ts).to include('export { useQuery } from "ruact/server-functions-runtime";')
197
- end
198
-
199
- it "raises a route×query collision when an action and a query share a js_identifier" do
200
- stub_const("CategoriesController", Class.new(ActionController::Base) { include Ruact::Server })
201
- stub_const("CollideQuery", Class.new(Ruact::Query) { def categories; end })
202
- rs = ActionDispatch::Routing::RouteSet.new
203
- rs.draw do
204
- post "categories", to: "categories#categories"
205
- ruact_queries CollideQuery
206
- end
207
- CategoriesController.define_singleton_method(:__ruact_function_name_overrides) do
208
- { "categories" => "categories" }
209
- end
210
- expect { Ruact::ServerFunctions.write_v2_snapshot!(route_set: rs, root: Pathname.new(@tmpdir)) }
211
- .to raise_error(Ruact::ConfigurationError, /both map to JS identifier "categories".*ruact_function_name/m)
212
- end
213
- end
214
- end
215
- end
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "rake"
5
- require "tmpdir"
6
- require "fileutils"
7
- require "action_controller"
8
- require "ruact/server"
9
-
10
- # Story 9.9 — exercises `rake ruact:server_functions:generate` end-to-end. The
11
- # task forces the route table to load, then writes the route-driven (v2)
12
- # snapshot to the REAL bridge (tmp/cache/ruact/server-functions.json) and
13
- # renders the TS module. It runs against a tmpdir as Rails.root with a stubbed
14
- # Rails.application providing a route set + a no-op routes_reloader.
15
- module Ruact
16
- module ServerFunctions
17
- RSpec.describe "rake ruact:server_functions:generate", :story_9_9 do
18
- before do
19
- stub_const("RakeDemoPostsController", Class.new(ActionController::Base) { include Ruact::Server })
20
- reloader = Object.new
21
- def reloader.execute_unless_loaded; end
22
- rs = ActionDispatch::Routing::RouteSet.new
23
- rs.draw { resources :rake_demo_posts, only: %i[create update destroy] }
24
- app = Object.new
25
- app.define_singleton_method(:routes) { rs }
26
- app.define_singleton_method(:routes_reloader) { reloader }
27
- allow(Rails).to receive(:application).and_return(app)
28
- end
29
-
30
- around do |example|
31
- Dir.mktmpdir do |dir|
32
- original_root = Rails.root
33
- Rails.root = Pathname.new(dir)
34
- @tmpdir = dir
35
-
36
- prev = Rake.application
37
- Rake.application = Rake::Application.new
38
- Rake.application.define_task(Rake::Task, :environment)
39
- load File.expand_path("../../../lib/tasks/ruact.rake", __dir__)
40
-
41
- example.run
42
- ensure
43
- Rake.application = prev
44
- Rails.root = original_root
45
- end
46
- end
47
-
48
- let(:json_path) { File.join(@tmpdir, "tmp/cache/ruact/server-functions.json") }
49
- let(:ts_path) { File.join(@tmpdir, "app/javascript/.ruact/server-functions.ts") }
50
-
51
- def invoke!
52
- Rake::Task["ruact:server_functions:generate"].reenable
53
- Rake::Task["ruact:server_functions:generate"].invoke
54
- end
55
-
56
- it "registers the task under the documented name" do
57
- expect(Rake.application.lookup("ruact:server_functions:generate")).not_to be_nil
58
- end
59
-
60
- it "writes both the v2 JSON bridge and the TS module on first run", :aggregate_failures do
61
- invoke!
62
- expect(File).to exist(json_path)
63
- expect(File).to exist(ts_path)
64
- expect(JSON.parse(File.read(json_path)).fetch("version")).to eq(2)
65
- expect(File.read(ts_path)).to include("// AUTO-GENERATED by vite-plugin-ruact")
66
- expect(File.read(ts_path)).to include("_makeServerFunction({ method: \"POST\"")
67
- end
68
-
69
- it "is idempotent: a second run does not change file contents" do
70
- invoke!
71
- before = File.read(ts_path)
72
- invoke!
73
- expect(File.read(ts_path)).to eq(before)
74
- end
75
-
76
- it "exits 1 with a `[ruact] error:` line when codegen raises a ConfigurationError" do
77
- allow(Ruact::ServerFunctions).to receive(:write_v2_snapshot!)
78
- .and_raise(Ruact::ConfigurationError,
79
- "server-function naming collision: PostsController#create and ...")
80
- expect { invoke! }
81
- .to raise_error(SystemExit)
82
- .and output(/\[ruact\] error:.*naming collision/).to_stderr
83
- end
84
- end
85
- end
86
- end