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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +123 -48
- data/CONTRIBUTING.md +233 -0
- data/README.md +16 -6
- data/RELEASING.md +125 -139
- data/lib/ruact/packaging.rb +68 -0
- data/lib/ruact/version.rb +1 -1
- data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +321 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +11 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +1 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +14 -3
- metadata +6 -107
- data/.codecov.yml +0 -31
- data/.github/workflows/ci.yml +0 -284
- data/.github/workflows/server-functions-bench.yml +0 -54
- data/.rubocop.yml +0 -107
- data/.rubocop_todo.yml +0 -63
- data/Rakefile +0 -10
- data/bench/server_functions_dispatch_bench.rb +0 -276
- data/bench/server_functions_dispatch_bench.results.md +0 -150
- data/docs/internal/README.md +0 -9
- data/docs/internal/decisions/server-functions-api.md +0 -2236
- data/spec/benchmarks/baseline.json +0 -1
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -92
- data/spec/fixtures/flight/README.md +0 -136
- data/spec/fixtures/flight/array.txt +0 -1
- data/spec/fixtures/flight/as_json_object.txt +0 -2
- data/spec/fixtures/flight/bigint.txt +0 -1
- data/spec/fixtures/flight/boolean_false.txt +0 -1
- data/spec/fixtures/flight/boolean_true.txt +0 -1
- data/spec/fixtures/flight/client_component_with_props.txt +0 -2
- data/spec/fixtures/flight/client_reference.txt +0 -2
- data/spec/fixtures/flight/hash.txt +0 -1
- data/spec/fixtures/flight/infinity.txt +0 -1
- data/spec/fixtures/flight/nan.txt +0 -1
- data/spec/fixtures/flight/negative_infinity.txt +0 -1
- data/spec/fixtures/flight/nil.txt +0 -1
- data/spec/fixtures/flight/number_float.txt +0 -1
- data/spec/fixtures/flight/number_integer.txt +0 -1
- data/spec/fixtures/flight/react_element_no_props.txt +0 -1
- data/spec/fixtures/flight/redirect_row.txt +0 -1
- data/spec/fixtures/flight/serializable_object.txt +0 -2
- data/spec/fixtures/flight/string_basic.txt +0 -1
- data/spec/fixtures/flight/string_dollar_escape.txt +0 -1
- data/spec/fixtures/flight/undefined.txt +0 -1
- data/spec/fixtures/readme/children-error.html.erb +0 -3
- data/spec/fixtures/readme/children-error.txt +0 -1
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +0 -16
- data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +0 -24
- data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +0 -15
- data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +0 -17
- data/spec/readme_demo_message_spec.rb +0 -67
- data/spec/readme_spec.rb +0 -282
- data/spec/ruact/client_manifest_spec.rb +0 -270
- data/spec/ruact/component_contract_spec.rb +0 -119
- data/spec/ruact/configuration_spec.rb +0 -518
- data/spec/ruact/controller_request_spec.rb +0 -671
- data/spec/ruact/controller_spec.rb +0 -343
- data/spec/ruact/doctor_spec.rb +0 -769
- data/spec/ruact/erb_preprocessor_hook_spec.rb +0 -55
- data/spec/ruact/erb_preprocessor_spec.rb +0 -361
- data/spec/ruact/errors_spec.rb +0 -93
- data/spec/ruact/flight/renderer_spec.rb +0 -133
- data/spec/ruact/flight/serializer_spec.rb +0 -494
- data/spec/ruact/html_converter_spec.rb +0 -375
- data/spec/ruact/install_generator_spec.rb +0 -1549
- data/spec/ruact/layout_source_spec.rb +0 -108
- data/spec/ruact/manifest_resolver_spec.rb +0 -174
- data/spec/ruact/query_request_spec.rb +0 -706
- data/spec/ruact/query_spec.rb +0 -105
- data/spec/ruact/railtie_spec.rb +0 -155
- data/spec/ruact/render_context_spec.rb +0 -58
- data/spec/ruact/render_pipeline_concurrency_spec.rb +0 -78
- data/spec/ruact/render_pipeline_spec.rb +0 -928
- data/spec/ruact/scaffold_generator_spec.rb +0 -1849
- data/spec/ruact/serializable_spec.rb +0 -179
- data/spec/ruact/server_bucket_request_spec.rb +0 -785
- data/spec/ruact/server_function_name_spec.rb +0 -53
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +0 -63
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +0 -200
- data/spec/ruact/server_functions/codegen_spec.rb +0 -397
- data/spec/ruact/server_functions/error_payload_spec.rb +0 -222
- data/spec/ruact/server_functions/error_suggestion_spec.rb +0 -79
- data/spec/ruact/server_functions/introspection_spec.rb +0 -135
- data/spec/ruact/server_functions/name_bridge_spec.rb +0 -212
- data/spec/ruact/server_functions/query_context_spec.rb +0 -72
- data/spec/ruact/server_functions/query_source_spec.rb +0 -193
- data/spec/ruact/server_functions/railtie_integration_spec.rb +0 -215
- data/spec/ruact/server_functions/rake_spec.rb +0 -86
- data/spec/ruact/server_functions/route_source_spec.rb +0 -202
- data/spec/ruact/server_functions/snapshot_spec.rb +0 -96
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +0 -71
- data/spec/ruact/server_rescue_request_spec.rb +0 -416
- data/spec/ruact/server_spec.rb +0 -179
- data/spec/ruact/server_upload_request_spec.rb +0 -311
- data/spec/ruact/signed_references_spec.rb +0 -164
- data/spec/ruact/string_distance_spec.rb +0 -38
- data/spec/ruact/tasks_json_introspection_spec.rb +0 -141
- data/spec/ruact/testing/have_ruact_component_spec.rb +0 -170
- data/spec/ruact/testing/no_production_load_spec.rb +0 -41
- data/spec/ruact/validation_errors_spec.rb +0 -116
- data/spec/ruact/view_helper_spec.rb +0 -131
- data/spec/spec_helper.rb +0 -77
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +0 -21
- data/spec/support/flight_wire_parser_spec.rb +0 -93
- data/spec/support/matchers/flight_fixture_matcher.rb +0 -130
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +0 -250
- 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
|